Spaces:
Sleeping
Sleeping
| 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(""" | |
| <style> | |
| /* 1. General Button Styling */ | |
| .stButton > button { | |
| width: 100% !important; | |
| border-radius: 12px; | |
| height: 3.5em; | |
| font-size: 18px; | |
| margin-bottom: 8px; | |
| } | |
| /* 2. BIGGER INPUT TEXT (For Mobile Editing) */ | |
| .stTextInput input, .stTextArea textarea { | |
| font-size: 20px !important; /* Bigger font for inputs */ | |
| line-height: 1.5 !important; | |
| } | |
| </style> | |
| """, 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""" | |
| <style> | |
| div[data-testid="stButton"] button[kind="primary"] {{ | |
| height: 300px !important; | |
| background-color: {card_bg} !important; | |
| color: {card_text} !important; | |
| border: 2px solid #e5e7eb !important; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1) !important; | |
| white-space: pre-wrap !important; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| transition: transform 0.1s; | |
| }} | |
| div[data-testid="stButton"] button[kind="primary"] p {{ | |
| font-size: {card_font_size} !important; | |
| font-weight: bold !important; | |
| }} | |
| </style> | |
| """, 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') |