import streamlit as st import genanki import uuid import base64 import fitz # PyMuPDF from youtube_transcript_api import YouTubeTranscriptApi from PIL import Image import io # === CONFIG === st.set_page_config(page_title="FlashCardX", layout="wide") # === THEME TOGGLE === if 'dark_mode' not in st.session_state: st.session_state.dark_mode = False def toggle_theme(): st.session_state.dark_mode = not st.session_state.dark_mode theme_btn = "๐ŸŒ™ Dark Mode" if not st.session_state.dark_mode else "โ˜€๏ธ Light Mode" st.sidebar.button(theme_btn, on_click=toggle_theme) bg_color = "#1e1e1e" if st.session_state.dark_mode else "#ffffff" txt_color = "#f1f1f1" if st.session_state.dark_mode else "#000000" st.markdown(f"""""", unsafe_allow_html=True) # === STATE === if 'deck' not in st.session_state: st.session_state.deck = [] # === HEADER === st.title("๐Ÿง  FlashCardX โ€” Flashka Clone (Offline, Free)") # === MANUAL Q/A FLASHCARD ENTRY === st.header("โž• Add Flashcard (Manual)") q = st.text_area("Question") a = st.text_area("Answer") if st.button("Add Flashcard"): if q.strip() and a.strip(): st.session_state.deck.append({'type': 'basic', 'q': q.strip(), 'a': a.strip()}) st.success("Card added.") else: st.warning("Both fields are required.") # === MCQ FLASHCARDS === st.header("๐Ÿ“ Add MCQ Card") mcq_q = st.text_input("MCQ Question") opt1 = st.text_input("Option A") opt2 = st.text_input("Option B") opt3 = st.text_input("Option C") opt4 = st.text_input("Option D") mcq_ans = st.selectbox("Correct Answer", ["A", "B", "C", "D"]) if st.button("Add MCQ"): if all([mcq_q, opt1, opt2, opt3, opt4]): st.session_state.deck.append({ 'type': 'mcq', 'q': mcq_q, 'options': [opt1, opt2, opt3, opt4], 'a': mcq_ans }) st.success("MCQ added.") else: st.warning("All options must be filled.") # === PDF UPLOAD TO FLASHCARDS === st.header("๐Ÿ“„ Upload PDF to Generate Flashcards") pdf_file = st.file_uploader("Upload PDF", type=['pdf']) if pdf_file: doc = fitz.open(stream=pdf_file.read(), filetype="pdf") for page in doc: text = page.get_text() lines = [l.strip() for l in text.split('\n') if l.strip()] for i in range(0, len(lines) - 1, 2): st.session_state.deck.append({'type': 'basic', 'q': lines[i], 'a': lines[i + 1]}) st.success("PDF parsed and flashcards added.") # === YOUTUBE TO FLASHCARDS === st.header("๐ŸŽฅ YouTube โ†’ Flashcards") yt_url = st.text_input("YouTube video link") if st.button("Extract Transcript Cards"): try: video_id = yt_url.split("v=")[-1].split("&")[0] transcript = YouTubeTranscriptApi.get_transcript(video_id) for item in transcript[:10]: # limit to 10 cards text = item['text'] st.session_state.deck.append({'type': 'basic', 'q': f"What is said here?", 'a': text}) st.success("Transcript converted to flashcards.") except Exception as e: st.error(f"Error: {e}") # === IMAGE OCCLUSION === st.header("๐Ÿ–ผ๏ธ Image Occlusion Flashcard") img_file = st.file_uploader("Upload Image", type=['png', 'jpg', 'jpeg']) if img_file: image = Image.open(img_file) st.image(image, caption="Original Image") label = st.text_input("Describe hidden info") if st.button("Add Image Occlusion Card"): img_bytes = io.BytesIO() image.save(img_bytes, format='PNG') b64_img = base64.b64encode(img_bytes.getvalue()).decode() q = f'
What is hidden?' a = label st.session_state.deck.append({'type': 'basic', 'q': q, 'a': a}) st.success("Image occlusion card added.") # === SEARCH === st.header("๐Ÿ” Search Deck") search_term = st.text_input("Enter keyword to filter") filtered_deck = [c for c in st.session_state.deck if search_term.lower() in c['q'].lower()] # === DECK VIEW === st.header("๐Ÿ“š Flashcard Deck Viewer") if filtered_deck: for i, card in enumerate(filtered_deck): with st.expander(f"Card {i+1}"): if card['type'] == 'basic': st.markdown(f"**Q:** {card['q']}", unsafe_allow_html=True) st.markdown(f"**A:** {card['a']}", unsafe_allow_html=True) elif card['type'] == 'mcq': st.markdown(f"**Q:** {card['q']}") for idx, opt in zip(['A', 'B', 'C', 'D'], card['options']): st.markdown(f"- {idx}. {opt}") st.markdown(f"**Answer:** {card['a']}") if st.button(f"๐Ÿ—‘๏ธ Delete", key=f"del_{i}"): st.session_state.deck.remove(card) st.experimental_rerun() else: st.info("No matching flashcards.") # === GAMIFIED REVIEW === st.header("๐ŸŽฎ Gamified Review") if st.button("Start Review"): for card in st.session_state.deck: if card['type'] == 'basic': st.info(f"Q: {card['q']}") st.text_input("Your Answer") st.markdown(f"**Correct:** {card['a']}") elif card['type'] == 'mcq': st.info(card['q']) choice = st.radio("Choose", ["A", "B", "C", "D"], key=uuid.uuid4()) st.markdown(f"**Answer:** {card['a']}") # === EXPORT TO ANKI === st.sidebar.header("๐Ÿ“ฅ Export to Anki") deck_name = st.sidebar.text_input("Deck Name", value="MyFlashDeck") if st.sidebar.button("Download .apkg"): model_id = int(uuid.uuid4()) >> 64 model = genanki.Model( model_id, 'FlashCardX Model', fields=[{'name': 'Question'}, {'name': 'Answer'}], templates=[{ 'name': 'Card Template', 'qfmt': '{{Question}}', 'afmt': '{{FrontSide}}
{{Answer}}', }], ) my_deck = genanki.Deck(int(uuid.uuid4()) >> 64, deck_name) for card in st.session_state.deck: if card['type'] == 'basic': my_deck.add_note(genanki.Note(model=model, fields=[card['q'], card['a']])) elif card['type'] == 'mcq': opts = "\n".join([f"{i}. {opt}" for i, opt in zip("ABCD", card['options'])]) q = f"{card['q']}\n\n{opts}" a = f"Correct Answer: {card['a']}" my_deck.add_note(genanki.Note(model=model, fields=[q, a])) pkg = genanki.Package(my_deck) fname = f"{deck_name.replace(' ', '_')}.apkg" pkg.write_to_file(fname) with open(fname, "rb") as f: b64 = base64.b64encode(f.read()).decode() href = f'๐Ÿ“ฆ Download {fname}' st.sidebar.markdown(href, unsafe_allow_html=True) # === FOOTER === st.markdown("---") st.caption("๐Ÿง  FlashCardX ยท A clean Flashka-style clone ยท Powered by Streamlit")