oceansweep's picture
Upload 169 files
c5b0bb7 verified
raw
history blame
36.5 kB
# Anki_Validation_tab.py
# Description: Gradio functions for the Anki Validation tab
#
# Imports
import json
import logging
import os
import tempfile
from typing import Optional, Tuple, List, Dict
#
# External Imports
import genanki
import gradio as gr
#
# Local Imports
from App_Function_Libraries.Chat.Chat_Functions import approximate_token_count, update_chat_content, save_chat_history, \
save_chat_history_to_db_wrapper
from App_Function_Libraries.DB.DB_Manager import list_prompts
from App_Function_Libraries.Gradio_UI.Chat_ui import update_dropdown_multiple, chat_wrapper, update_selected_parts, \
search_conversations, regenerate_last_message, load_conversation, debug_output
from App_Function_Libraries.Third_Party.Anki import sanitize_html, generate_card_choices, \
export_cards, load_card_for_editing, handle_file_upload, \
validate_for_ui, update_card_with_validation, update_card_choices, enhanced_file_upload, \
handle_validation
from App_Function_Libraries.Utils.Utils import default_api_endpoint, global_api_endpoints, format_api_name
#
############################################################################################################
#
# Functions:
def create_anki_validation_tab():
with gr.TabItem("Anki Flashcard Validation", visible=True):
gr.Markdown("# Anki Flashcard Validation and Editor")
# State variables for internal tracking
current_card_data = gr.State({})
preview_update_flag = gr.State(False)
with gr.Row():
# Left Column: Input and Validation
with gr.Column(scale=1):
gr.Markdown("## Import or Create Flashcards")
input_type = gr.Radio(
choices=["JSON", "APKG"],
label="Input Type",
value="JSON"
)
with gr.Group() as json_input_group:
flashcard_input = gr.TextArea(
label="Enter Flashcards (JSON format)",
placeholder='''{
"cards": [
{
"id": "CARD_001",
"type": "basic",
"front": "What is the capital of France?",
"back": "Paris",
"tags": ["geography", "europe"],
"note": "Remember: City of Light"
}
]
}''',
lines=10
)
import_json = gr.File(
label="Or Import JSON File",
file_types=[".json"]
)
with gr.Group(visible=False) as apkg_input_group:
import_apkg = gr.File(
label="Import APKG File",
file_types=[".apkg"]
)
deck_info = gr.JSON(
label="Deck Information",
visible=False
)
validate_button = gr.Button("Validate Flashcards")
# Right Column: Validation Results and Editor
with gr.Column(scale=1):
gr.Markdown("## Validation Results")
validation_status = gr.Markdown("")
with gr.Accordion("Validation Rules", open=False):
gr.Markdown("""
### Required Fields:
- Unique ID
- Card Type (basic, cloze, reverse)
- Front content
- Back content
- At least one tag
### Content Rules:
- No empty fields
- Front side should be a clear question/prompt
- Back side should contain complete answer
- Cloze deletions must have valid syntax
- No duplicate IDs
### Image Rules:
- Valid image tags
- Supported formats (JPG, PNG, GIF)
- Base64 encoded or valid URL
### APKG-specific Rules:
- Valid SQLite database structure
- Media files properly referenced
- Note types match Anki standards
- Card templates are well-formed
""")
with gr.Row():
# Card Editor
gr.Markdown("## Card Editor")
with gr.Row():
with gr.Column(scale=1):
with gr.Accordion("Edit Individual Cards", open=True):
card_selector = gr.Dropdown(
label="Select Card to Edit",
choices=[],
interactive=True
)
card_type = gr.Radio(
choices=["basic", "cloze", "reverse"],
label="Card Type",
value="basic"
)
# Front content with preview
with gr.Group():
gr.Markdown("### Front Content")
front_content = gr.TextArea(
label="Content (HTML supported)",
lines=3
)
front_preview = gr.HTML(
label="Preview"
)
# Back content with preview
with gr.Group():
gr.Markdown("### Back Content")
back_content = gr.TextArea(
label="Content (HTML supported)",
lines=3
)
back_preview = gr.HTML(
label="Preview"
)
tags_input = gr.TextArea(
label="Tags (comma-separated)",
lines=1
)
notes_input = gr.TextArea(
label="Additional Notes",
lines=2
)
with gr.Row():
update_card_button = gr.Button("Update Card")
delete_card_button = gr.Button("Delete Card", variant="stop")
with gr.Row():
with gr.Column(scale=1):
# Export Options
gr.Markdown("## Export Options")
export_format = gr.Radio(
choices=["Anki CSV", "JSON", "Plain Text"],
label="Export Format",
value="Anki CSV"
)
export_button = gr.Button("Export Valid Cards")
export_file = gr.File(label="Download Validated Cards")
export_status = gr.Markdown("")
with gr.Column(scale=1):
gr.Markdown("## Export Instructions")
gr.Markdown("""
### Anki CSV Format:
- Front, Back, Tags, Type, Note
- Use for importing into Anki
- Images preserved as HTML
### JSON Format:
- JSON array of cards
- Images as base64 or URLs
- Use for custom processing
### Plain Text Format:
- Question and Answer pairs
- Images represented as [IMG] placeholder
- Use for manual review
""")
def update_preview(content):
"""Update preview with sanitized content."""
if not content:
return ""
return sanitize_html(content)
# Event handlers
def validation_chain(content: str) -> Tuple[str, List[str]]:
"""Combined validation and card choice update."""
validation_message = validate_for_ui(content)
card_choices = update_card_choices(content)
return validation_message, card_choices
def delete_card(card_selection, current_content):
"""Delete selected card and return updated content."""
if not card_selection or not current_content:
return current_content, "No card selected", []
try:
data = json.loads(current_content)
selected_id = card_selection.split(" - ")[0]
data['cards'] = [card for card in data['cards'] if card['id'] != selected_id]
new_content = json.dumps(data, indent=2)
return (
new_content,
"Card deleted successfully!",
generate_card_choices(new_content)
)
except Exception as e:
return current_content, f"Error deleting card: {str(e)}", []
def process_validation_result(is_valid, message):
"""Process validation result into a formatted markdown string."""
if is_valid:
return f"βœ… {message}"
else:
return f"❌ {message}"
# Register event handlers
input_type.change(
fn=lambda t: (
gr.update(visible=t == "JSON"),
gr.update(visible=t == "APKG"),
gr.update(visible=t == "APKG")
),
inputs=[input_type],
outputs=[json_input_group, apkg_input_group, deck_info]
)
# File upload handlers
import_json.upload(
fn=handle_file_upload,
inputs=[import_json, input_type],
outputs=[
flashcard_input,
deck_info,
validation_status,
card_selector
]
)
import_apkg.upload(
fn=enhanced_file_upload,
inputs=[import_apkg, input_type],
outputs=[
flashcard_input,
deck_info,
validation_status,
card_selector
]
)
# Validation handler
validate_button.click(
fn=lambda content, input_format: (
handle_validation(content, input_format),
generate_card_choices(content) if content else []
),
inputs=[flashcard_input, input_type],
outputs=[validation_status, card_selector]
)
# Card editing handlers
# Card selector change event
card_selector.change(
fn=load_card_for_editing,
inputs=[card_selector, flashcard_input],
outputs=[
card_type,
front_content,
back_content,
tags_input,
notes_input,
front_preview,
back_preview
]
)
# Live preview updates
front_content.change(
fn=update_preview,
inputs=[front_content],
outputs=[front_preview]
)
back_content.change(
fn=update_preview,
inputs=[back_content],
outputs=[back_preview]
)
# Card update handler
update_card_button.click(
fn=update_card_with_validation,
inputs=[
flashcard_input,
card_selector,
card_type,
front_content,
back_content,
tags_input,
notes_input
],
outputs=[
flashcard_input,
validation_status,
card_selector
]
)
# Delete card handler
delete_card_button.click(
fn=delete_card,
inputs=[card_selector, flashcard_input],
outputs=[flashcard_input, validation_status, card_selector]
)
# Export handler
export_button.click(
fn=export_cards,
inputs=[flashcard_input, export_format],
outputs=[export_status, export_file]
)
return (
flashcard_input,
import_json,
import_apkg,
validate_button,
validation_status,
card_selector,
card_type,
front_content,
back_content,
front_preview,
back_preview,
tags_input,
notes_input,
update_card_button,
delete_card_button,
export_format,
export_button,
export_file,
export_status,
deck_info
)
def create_anki_generator_tab():
with gr.TabItem("Anki Deck Generator", visible=True):
try:
default_value = None
if default_api_endpoint:
if default_api_endpoint in global_api_endpoints:
default_value = format_api_name(default_api_endpoint)
else:
logging.warning(f"Default API endpoint '{default_api_endpoint}' not found in global_api_endpoints")
except Exception as e:
logging.error(f"Error setting default API endpoint: {str(e)}")
default_value = None
custom_css = """
.chatbot-container .message-wrap .message {
font-size: 14px !important;
}
"""
with gr.TabItem("LLM Chat & Anki Deck Creation", visible=True):
gr.Markdown("# Chat with an LLM to help you come up with Questions/Answers for an Anki Deck")
chat_history = gr.State([])
media_content = gr.State({})
selected_parts = gr.State([])
conversation_id = gr.State(None)
initial_prompts, total_pages, current_page = list_prompts(page=1, per_page=10)
with gr.Row():
with gr.Column(scale=1):
search_query_input = gr.Textbox(
label="Search Query",
placeholder="Enter your search query here..."
)
search_type_input = gr.Radio(
choices=["Title", "Content", "Author", "Keyword"],
value="Keyword",
label="Search By"
)
keyword_filter_input = gr.Textbox(
label="Filter by Keywords (comma-separated)",
placeholder="ml, ai, python, etc..."
)
search_button = gr.Button("Search")
items_output = gr.Dropdown(label="Select Item", choices=[], interactive=True)
item_mapping = gr.State({})
with gr.Row():
use_content = gr.Checkbox(label="Use Content")
use_summary = gr.Checkbox(label="Use Summary")
use_prompt = gr.Checkbox(label="Use Prompt")
save_conversation = gr.Checkbox(label="Save Conversation", value=False, visible=True)
with gr.Row():
temperature = gr.Slider(label="Temperature", minimum=0.00, maximum=1.0, step=0.05, value=0.7)
with gr.Row():
conversation_search = gr.Textbox(label="Search Conversations")
with gr.Row():
search_conversations_btn = gr.Button("Search Conversations")
with gr.Row():
previous_conversations = gr.Dropdown(label="Select Conversation", choices=[], interactive=True)
with gr.Row():
load_conversations_btn = gr.Button("Load Selected Conversation")
# Refactored API selection dropdown
api_endpoint = gr.Dropdown(
choices=["None"] + [format_api_name(api) for api in global_api_endpoints],
value=default_value,
label="API for Chat Interaction (Optional)"
)
api_key = gr.Textbox(label="API Key (if required)", type="password")
custom_prompt_checkbox = gr.Checkbox(label="Use a Custom Prompt",
value=False,
visible=True)
preset_prompt_checkbox = gr.Checkbox(label="Use a Pre-set Prompt",
value=False,
visible=True)
with gr.Row(visible=False) as preset_prompt_controls:
prev_prompt_page = gr.Button("Previous")
next_prompt_page = gr.Button("Next")
current_prompt_page_text = gr.Text(f"Page {current_page} of {total_pages}")
current_prompt_page_state = gr.State(value=1)
preset_prompt = gr.Dropdown(
label="Select Preset Prompt",
choices=initial_prompts
)
user_prompt = gr.Textbox(label="Custom Prompt",
placeholder="Enter custom prompt here",
lines=3,
visible=False)
system_prompt_input = gr.Textbox(label="System Prompt",
value="You are a helpful AI assitant",
lines=3,
visible=False)
with gr.Column(scale=2):
chatbot = gr.Chatbot(height=800, elem_classes="chatbot-container")
msg = gr.Textbox(label="Enter your message")
submit = gr.Button("Submit")
regenerate_button = gr.Button("Regenerate Last Message")
token_count_display = gr.Number(label="Approximate Token Count", value=0, interactive=False)
clear_chat_button = gr.Button("Clear Chat")
chat_media_name = gr.Textbox(label="Custom Chat Name(optional)")
save_chat_history_to_db = gr.Button("Save Chat History to DataBase")
save_status = gr.Textbox(label="Save Status", interactive=False)
save_chat_history_as_file = gr.Button("Save Chat History as File")
download_file = gr.File(label="Download Chat History")
search_button.click(
fn=update_dropdown_multiple,
inputs=[search_query_input, search_type_input, keyword_filter_input],
outputs=[items_output, item_mapping]
)
def update_prompt_visibility(custom_prompt_checked, preset_prompt_checked):
user_prompt_visible = custom_prompt_checked
system_prompt_visible = custom_prompt_checked
preset_prompt_visible = preset_prompt_checked
preset_prompt_controls_visible = preset_prompt_checked
return (
gr.update(visible=user_prompt_visible, interactive=user_prompt_visible),
gr.update(visible=system_prompt_visible, interactive=system_prompt_visible),
gr.update(visible=preset_prompt_visible, interactive=preset_prompt_visible),
gr.update(visible=preset_prompt_controls_visible)
)
def update_prompt_page(direction, current_page_val):
new_page = current_page_val + direction
if new_page < 1:
new_page = 1
prompts, total_pages, _ = list_prompts(page=new_page, per_page=20)
if new_page > total_pages:
new_page = total_pages
prompts, total_pages, _ = list_prompts(page=new_page, per_page=20)
return (
gr.update(choices=prompts),
gr.update(value=f"Page {new_page} of {total_pages}"),
new_page
)
def clear_chat():
return [], None # Return empty list for chatbot and None for conversation_id
custom_prompt_checkbox.change(
update_prompt_visibility,
inputs=[custom_prompt_checkbox, preset_prompt_checkbox],
outputs=[user_prompt, system_prompt_input, preset_prompt, preset_prompt_controls]
)
preset_prompt_checkbox.change(
update_prompt_visibility,
inputs=[custom_prompt_checkbox, preset_prompt_checkbox],
outputs=[user_prompt, system_prompt_input, preset_prompt, preset_prompt_controls]
)
prev_prompt_page.click(
lambda x: update_prompt_page(-1, x),
inputs=[current_prompt_page_state],
outputs=[preset_prompt, current_prompt_page_text, current_prompt_page_state]
)
next_prompt_page.click(
lambda x: update_prompt_page(1, x),
inputs=[current_prompt_page_state],
outputs=[preset_prompt, current_prompt_page_text, current_prompt_page_state]
)
submit.click(
chat_wrapper,
inputs=[msg, chatbot, media_content, selected_parts, api_endpoint, api_key, user_prompt,
conversation_id,
save_conversation, temperature, system_prompt_input],
outputs=[msg, chatbot, conversation_id]
).then( # Clear the message box after submission
lambda x: gr.update(value=""),
inputs=[chatbot],
outputs=[msg]
).then( # Clear the user prompt after the first message
lambda: (gr.update(value=""), gr.update(value="")),
outputs=[user_prompt, system_prompt_input]
).then(
lambda history: approximate_token_count(history),
inputs=[chatbot],
outputs=[token_count_display]
)
clear_chat_button.click(
clear_chat,
outputs=[chatbot, conversation_id]
)
items_output.change(
update_chat_content,
inputs=[items_output, use_content, use_summary, use_prompt, item_mapping],
outputs=[media_content, selected_parts]
)
use_content.change(update_selected_parts, inputs=[use_content, use_summary, use_prompt],
outputs=[selected_parts])
use_summary.change(update_selected_parts, inputs=[use_content, use_summary, use_prompt],
outputs=[selected_parts])
use_prompt.change(update_selected_parts, inputs=[use_content, use_summary, use_prompt],
outputs=[selected_parts])
items_output.change(debug_output, inputs=[media_content, selected_parts], outputs=[])
search_conversations_btn.click(
search_conversations,
inputs=[conversation_search],
outputs=[previous_conversations]
)
load_conversations_btn.click(
clear_chat,
outputs=[chatbot, chat_history]
).then(
load_conversation,
inputs=[previous_conversations],
outputs=[chatbot, conversation_id]
)
previous_conversations.change(
load_conversation,
inputs=[previous_conversations],
outputs=[chat_history]
)
save_chat_history_as_file.click(
save_chat_history,
inputs=[chatbot, conversation_id],
outputs=[download_file]
)
save_chat_history_to_db.click(
save_chat_history_to_db_wrapper,
inputs=[chatbot, conversation_id, media_content, chat_media_name],
outputs=[conversation_id, gr.Textbox(label="Save Status")]
)
regenerate_button.click(
regenerate_last_message,
inputs=[chatbot, media_content, selected_parts, api_endpoint, api_key, user_prompt, temperature,
system_prompt_input],
outputs=[chatbot, save_status]
).then(
lambda history: approximate_token_count(history),
inputs=[chatbot],
outputs=[token_count_display]
)
gr.Markdown("# Create Anki Deck")
with gr.Row():
# Left Column: Deck Settings
with gr.Column(scale=1):
gr.Markdown("## Deck Settings")
deck_name = gr.Textbox(
label="Deck Name",
placeholder="My Study Deck",
value="My Study Deck"
)
deck_description = gr.Textbox(
label="Deck Description",
placeholder="Description of your deck",
lines=2
)
note_type = gr.Radio(
choices=["Basic", "Basic (and reversed)", "Cloze"],
label="Note Type",
value="Basic"
)
# Card Fields based on note type
with gr.Group() as basic_fields:
front_template = gr.Textbox(
label="Front Template (HTML)",
value="{{Front}}",
lines=3
)
back_template = gr.Textbox(
label="Back Template (HTML)",
value="{{FrontSide}}<hr id='answer'>{{Back}}",
lines=3
)
with gr.Group() as cloze_fields:
cloze_template = gr.Textbox(
label="Cloze Template (HTML)",
value="{{cloze:Text}}",
lines=3,
visible=False
)
css_styling = gr.Textbox(
label="Card Styling (CSS)",
value=".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n\n.cloze {\n font-weight: bold;\n color: blue;\n}",
lines=5
)
# Right Column: Card Creation
with gr.Column(scale=1):
gr.Markdown("## Add Cards")
with gr.Group() as basic_input:
front_content = gr.TextArea(
label="Front Content",
placeholder="Question or prompt",
lines=3
)
back_content = gr.TextArea(
label="Back Content",
placeholder="Answer",
lines=3
)
with gr.Group() as cloze_input:
cloze_content = gr.TextArea(
label="Cloze Content",
placeholder="Text with {{c1::cloze}} deletions",
lines=3,
visible=False
)
tags_input = gr.TextArea(
label="Tags (comma-separated)",
placeholder="tag1, tag2, tag3",
lines=1
)
add_card_btn = gr.Button("Add Card")
cards_list = gr.JSON(
label="Cards in Deck",
value={"cards": []}
)
clear_cards_btn = gr.Button("Clear All Cards", variant="stop")
with gr.Row():
generate_deck_btn = gr.Button("Generate Deck", variant="primary")
download_deck = gr.File(label="Download Deck")
generation_status = gr.Markdown("")
def update_note_type_fields(note_type: str):
if note_type == "Cloze":
return {
basic_input: gr.update(visible=False),
cloze_input: gr.update(visible=True),
basic_fields: gr.update(visible=False),
cloze_fields: gr.update(visible=True)
}
else:
return {
basic_input: gr.update(visible=True),
cloze_input: gr.update(visible=False),
basic_fields: gr.update(visible=True),
cloze_fields: gr.update(visible=False)
}
def add_card(note_type: str, front: str, back: str, cloze: str, tags: str, current_cards: Dict[str, List]):
if not current_cards:
current_cards = {"cards": []}
cards_data = current_cards["cards"]
# Process tags
card_tags = [tag.strip() for tag in tags.split(',') if tag.strip()]
new_card = {
"id": f"CARD_{len(cards_data) + 1}",
"tags": card_tags
}
if note_type == "Cloze":
if not cloze or "{{c" not in cloze:
return current_cards, "❌ Invalid cloze format. Use {{c1::text}} syntax."
new_card.update({
"type": "cloze",
"content": cloze
})
else:
if not front or not back:
return current_cards, "❌ Both front and back content are required."
new_card.update({
"type": "basic",
"front": front,
"back": back,
"is_reverse": note_type == "Basic (and reversed)"
})
cards_data.append(new_card)
return {"cards": cards_data}, "βœ… Card added successfully!"
def clear_cards() -> Tuple[Dict[str, List], str]:
return {"cards": []}, "βœ… All cards cleared!"
def generate_anki_deck(
deck_name: str,
deck_description: str,
note_type: str,
front_template: str,
back_template: str,
cloze_template: str,
css: str,
cards_data: Dict[str, List]
) -> Tuple[Optional[str], str]:
try:
if not cards_data or not cards_data.get("cards"):
return None, "❌ No cards to generate deck from!"
# Create model based on note type
if note_type == "Cloze":
model = genanki.Model(
1483883320, # Random model ID
'Cloze Model',
fields=[
{'name': 'Text'},
{'name': 'Back Extra'}
],
templates=[{
'name': 'Cloze Card',
'qfmt': cloze_template,
'afmt': cloze_template + '<br><hr id="extra">{{Back Extra}}'
}],
css=css,
# FIXME CLOZE DOESNT EXIST
model_type=1
)
else:
templates = [{
'name': 'Card 1',
'qfmt': front_template,
'afmt': back_template
}]
if note_type == "Basic (and reversed)":
templates.append({
'name': 'Card 2',
'qfmt': '{{Back}}',
'afmt': '{{FrontSide}}<hr id="answer">{{Front}}'
})
model = genanki.Model(
1607392319, # Random model ID
'Basic Model',
fields=[
{'name': 'Front'},
{'name': 'Back'}
],
templates=templates,
css=css
)
# Create deck
deck = genanki.Deck(
2059400110, # Random deck ID
deck_name,
description=deck_description
)
# Add cards to deck
for card in cards_data["cards"]:
if card["type"] == "cloze":
note = genanki.Note(
model=model,
fields=[card["content"], ""],
tags=card["tags"]
)
else:
note = genanki.Note(
model=model,
fields=[card["front"], card["back"]],
tags=card["tags"]
)
deck.add_note(note)
# Save deck to temporary file
temp_dir = tempfile.mkdtemp()
deck_path = os.path.join(temp_dir, f"{deck_name}.apkg")
genanki.Package(deck).write_to_file(deck_path)
return deck_path, "βœ… Deck generated successfully!"
except Exception as e:
return None, f"❌ Error generating deck: {str(e)}"
# Register event handlers
note_type.change(
fn=update_note_type_fields,
inputs=[note_type],
outputs=[basic_input, cloze_input, basic_fields, cloze_fields]
)
add_card_btn.click(
fn=add_card,
inputs=[
note_type,
front_content,
back_content,
cloze_content,
tags_input,
cards_list
],
outputs=[cards_list, generation_status]
)
clear_cards_btn.click(
fn=clear_cards,
inputs=[],
outputs=[cards_list, generation_status]
)
generate_deck_btn.click(
fn=generate_anki_deck,
inputs=[
deck_name,
deck_description,
note_type,
front_template,
back_template,
cloze_template,
css_styling,
cards_list
],
outputs=[download_deck, generation_status]
)
return (
deck_name,
deck_description,
note_type,
front_template,
back_template,
cloze_template,
css_styling,
front_content,
back_content,
cloze_content,
tags_input,
cards_list,
add_card_btn,
clear_cards_btn,
generate_deck_btn,
download_deck,
generation_status
)
#
# End of Anki_Validation_tab.py
############################################################################################################