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')