import streamlit as st import pandas as pd from huggingface_hub import HfApi, hf_hub_download from io import StringIO import random import os # --- CONFIGURATION --- DATASET_NAME = "my-french-flashcards-data" CSV_FILE = "cards.csv" # --- BACKEND FUNCTIONS --- def get_api(): token = os.environ.get("HF_TOKEN") if not token: st.error("HF_TOKEN not found! Please add it in Space Settings -> Secrets.") st.stop() return HfApi(token=token), token def get_user_repo_id(api, token): user_info = api.whoami(token=token) return f"{user_info['name']}/{DATASET_NAME}" def load_data(): api, token = get_api() repo_id = get_user_repo_id(api, token) try: local_path = hf_hub_download( repo_id=repo_id, filename=CSV_FILE, repo_type="dataset", token=token ) return pd.read_csv(local_path) except Exception: try: api.create_repo(repo_id=repo_id, repo_type="dataset", private=True, exist_ok=True) except Exception as e: st.error(f"Could not create storage repository: {e}") return pd.DataFrame(columns=["french", "context", "english", "active"]) def save_data(df): api, token = get_api() repo_id = get_user_repo_id(api, token) csv_buffer = StringIO() df.to_csv(csv_buffer, index=False) csv_buffer.seek(0) try: api.upload_file( path_or_fileobj=csv_buffer.read().encode(), path_in_repo=CSV_FILE, repo_id=repo_id, repo_type="dataset", commit_message="Update flashcards" ) return True except Exception as e: st.error(f"Error saving data: {e}") return False # --- UI LOGIC --- if 'page' not in st.session_state: st.session_state['page'] = 'home' if 'df' not in st.session_state: st.session_state['df'] = load_data() if 'deck' not in st.session_state: st.session_state['deck'] = [] if 'current_card_idx' not in st.session_state: st.session_state['current_card_idx'] = 0 if 'side_cycle_count' not in st.session_state: st.session_state['side_cycle_count'] = 0 def set_page(page_name): st.session_state['page'] = page_name def start_session(): df = st.session_state['df'] if 'active' not in df.columns: df['active'] = True active_indices = df[df['active'] == True].index.tolist() if not active_indices: st.warning("No active cards found! Add some first.") return random.shuffle(active_indices) st.session_state['deck'] = active_indices st.session_state['current_card_idx'] = 0 st.session_state['side_cycle_count'] = 0 set_page('play') def next_card(): st.session_state['current_card_idx'] += 1 st.session_state['side_cycle_count'] = 0 if st.session_state['current_card_idx'] >= len(st.session_state['deck']): st.balloons() st.success("Session Complete!") set_page('home') def archive_current_card(): current_deck_idx = st.session_state['current_card_idx'] if current_deck_idx < len(st.session_state['deck']): df_idx = st.session_state['deck'][current_deck_idx] st.session_state['df'].at[df_idx, 'active'] = False save_data(st.session_state['df']) next_card() def save_edited_card(df_idx, new_fr, new_ctx, new_en): st.session_state['df'].at[df_idx, 'french'] = new_fr st.session_state['df'].at[df_idx, 'context'] = new_ctx st.session_state['df'].at[df_idx, 'english'] = new_en save_data(st.session_state['df']) st.success("Card Updated!") set_page('play') def add_card(french, context, english): new_row = pd.DataFrame({ "french": [french], "context": [context], "english": [english], "active": [True] }) st.session_state['df'] = pd.concat([st.session_state['df'], new_row], ignore_index=True) save_data(st.session_state['df']) st.success("Card Added!") def unarchive(df_idx): st.session_state['df'].at[df_idx, 'active'] = True save_data(st.session_state['df']) # --- PAGE RENDERING --- st.set_page_config(page_title="French Flashcards", page_icon="🇫🇷", layout="centered") # --- GLOBAL CSS --- st.markdown(""" """, unsafe_allow_html=True) if st.session_state['page'] == 'home': st.title("🇫🇷 French Practice") st.write(f"Total Cards: {len(st.session_state['df'])}") st.button("Start Session", on_click=start_session, use_container_width=True) st.button("Add Card", on_click=lambda: set_page('add'), use_container_width=True) st.button("View Archive", on_click=lambda: set_page('archive'), use_container_width=True) elif st.session_state['page'] == 'add': st.title("Add New Card") with st.form("add_card_form"): fr = st.text_input("French Word/Phrase") ctx = st.text_area("Context / Sentence") en = st.text_input("English Translation") submitted = st.form_submit_button("Save Card", use_container_width=True) if submitted and fr and en: add_card(fr, ctx, en) st.button("Return", on_click=lambda: set_page('home'), use_container_width=True) elif st.session_state['page'] == 'edit': st.title("Edit Card") deck_idx = st.session_state['current_card_idx'] df_idx = st.session_state['deck'][deck_idx] row = st.session_state['df'].iloc[df_idx] with st.form("edit_card_form"): new_fr = st.text_input("French Word/Phrase", value=row['french']) new_ctx = st.text_area("Context / Sentence", value=row['context']) new_en = st.text_input("English Translation", value=row['english']) saved = st.form_submit_button("Save Changes", use_container_width=True) if saved: save_edited_card(df_idx, new_fr, new_ctx, new_en) st.button("Return", on_click=lambda: set_page('play'), use_container_width=True) elif st.session_state['page'] == 'archive': st.title("Archive") st.button("Return", on_click=lambda: set_page('home'), use_container_width=True) df = st.session_state['df'] if not df.empty: st.write("Below are your inactive cards.") archived = df[df['active'] == False] if archived.empty: st.info("Archive is empty.") else: for idx, row in archived.iterrows(): st.markdown(f"**{row['french']}** — *{row['english']}*") st.button("Unarchive", key=f"unarc_{idx}", on_click=unarchive, args=(idx,), use_container_width=True) st.divider() else: st.write("No cards yet.") elif st.session_state['page'] == 'play': deck_idx = st.session_state['current_card_idx'] if deck_idx < len(st.session_state['deck']): df_idx = st.session_state['deck'][deck_idx] card = st.session_state['df'].iloc[df_idx] # Cycle: 0:Fr, 1:Ctx, 2:En, 3:Ctx cycle_map = {0: 'French', 1: 'Context', 2: 'English', 3: 'Context'} current_side_idx = st.session_state['side_cycle_count'] % 4 side_name = cycle_map[current_side_idx] content = "" # Defaults card_bg = "#ffffff" card_text = "#000000" card_font_size = "32px" if side_name == 'French': content = card['french'] card_bg = "#ffffff" # White card_text = "#1f2937" # Dark Grey elif side_name == 'Context': content = card['context'] card_bg = "#3b82f6" # Blue card_text = "#ffffff" # White card_font_size = "20px" # Smaller for long text elif side_name == 'English': content = card['english'] card_bg = "#ef4444" # Red card_text = "#ffffff" # White progress = (deck_idx + 1) / len(st.session_state['deck']) st.progress(progress) # --- DYNAMIC STYLE INJECTION --- # This overwrites the 'Primary' button style JUST for this render loop st.markdown(f""" """, unsafe_allow_html=True) # --- THE CLICKABLE CARD --- def cycle_side(): st.session_state['side_cycle_count'] += 1 # Just the content, no label st.button(content, on_click=cycle_side, type="primary", use_container_width=True) # --- NAVIGATION --- st.button("Next Card", on_click=next_card, use_container_width=True) st.divider() st.button("Archive this Card", on_click=archive_current_card, use_container_width=True) st.button("Edit Card", on_click=lambda: set_page('edit'), use_container_width=True) st.button("Quit Session", on_click=lambda: set_page('home'), use_container_width=True) else: set_page('home')