import streamlit as st
import tempfile
import json
import random
from pathlib import Path
from PyPDF2 import PdfReader
from openai import OpenAI
import os
from ast import literal_eval
# Import video processing functions
from video import (
process_uploaded_video,
generate_video_summary,
chat_with_video
)
# Initialize the OpenAI client
api_key = os.getenv("OPENAI_API_KEY")
client = OpenAI(api_key = api_key)
# ---------------------------
# Helper Function: Extract text from PDF
# ---------------------------
def extract_text(uploaded_file):
# Check file size (max 10MB)
uploaded_file.seek(0, os.SEEK_END)
file_size = uploaded_file.tell()
uploaded_file.seek(0)
if file_size > 10 * 1024 * 1024:
st.error("File size exceeds 10MB limit.")
return ""
pdf_reader = PdfReader(uploaded_file)
text = ""
for page in pdf_reader.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
return text
# ---------------------------
# OpenAI Response Functions (using new style)
# ---------------------------
def generate_summary_from_text(text):
prompt = (
f"Summarize the following document in a concise manner, highlighting the key points that a student should know:\n\n{text}"
)
messages = [
{"role": "system", "content": "You are an educational assistant."},
{"role": "user", "content": prompt}
]
completion = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
return completion.choices[0].message.content.strip()
def chat_with_document(text, conversation_history, user_query):
messages = conversation_history + [
{"role": "user", "content": f"Based on the following document:\n\n{text}\n\nQuestion: {user_query}"}
]
completion = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
return completion.choices[0].message.content.strip()
def generate_questions_from_text(text, num_questions):
prompt = (
f"Generate up to {num_questions} study questions with answers based on the following document.\n"
f"Return the output as a table with two columns: 'Question' and 'Answer'.\n\nDocument:\n\n{text}"
)
messages = [
{"role": "system", "content": "You are an educational assistant that generates study questions."},
{"role": "user", "content": prompt}
]
completion = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
return completion.choices[0].message.content.strip()
def generate_flashcards_from_text(text, num_cards):
prompt = (
f"Generate exactly {num_cards} flashcards based on the following document.\n\n"
f"Document:\n\n{text}\n\n"
"Return ONLY a valid JSON object (not a code block) where each key is a flashcard question and its value is the answer. "
"Format: {\"Question 1?\": \"Answer 1\", \"Question 2?\": \"Answer 2\"}. "
"Do not include ```json or any other text, just the raw JSON object."
)
messages = [
{"role": "system", "content": "You are an educational assistant that creates study flashcards. Always return valid JSON only, without code blocks or additional text."},
{"role": "user", "content": prompt}
]
completion = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
output = completion.choices[0].message.content.strip()
# Clean up the output - remove code block markers if present
if output.startswith("```json"):
output = output[7:] # Remove ```json
elif output.startswith("```"):
output = output[3:] # Remove ```
if output.endswith("```"):
output = output[:-3] # Remove trailing ```
output = output.strip()
try:
# Try JSON first (more reliable)
flashcards = json.loads(output)
if isinstance(flashcards, dict) and len(flashcards) > 0:
return flashcards
else:
st.error("Generated flashcards are empty or invalid format.")
return {}
except json.JSONDecodeError:
# Fallback to literal_eval for Python dict syntax
try:
flashcards = literal_eval(output)
if isinstance(flashcards, dict) and len(flashcards) > 0:
return flashcards
else:
st.error("Generated flashcards are empty or invalid format.")
return {}
except Exception as e:
st.error(f"Error parsing flashcards: {e}")
st.error(f"Received output: {output[:200]}...") # Show first 200 chars for debugging
return {}
# ---------------------------
# Sidebar: File Upload & Mode Selection
# ---------------------------
st.sidebar.markdown("""
""", unsafe_allow_html=True)
# Input source selection
input_source = st.sidebar.radio("📥 Select Input Source", ("PDF Document", "Video"))
st.sidebar.markdown("---")
# Initialize variables
uploaded_pdf = None
uploaded_video = None
if input_source == "PDF Document":
uploaded_pdf = st.sidebar.file_uploader("📄 Upload your study PDF (max 10MB)", type="pdf")
mode = st.sidebar.radio("🎯 Select Mode", ("Chat", "Test Your Knowledge", "Flashcards"))
elif input_source == "Video":
uploaded_video = st.sidebar.file_uploader("🎥 Upload your video (MP4, max 200MB)", type=["mp4", "mkv", "mov", "avi"])
mode = st.sidebar.radio("🎯 Select Mode", ("Transcript", "Summary", "Chat"))
st.sidebar.markdown("---")
# For Test Your Knowledge and Flashcards modes, allow number input.
num_questions = None
num_flashcards = None
if input_source == "PDF Document":
if mode == "Test Your Knowledge":
num_questions = st.sidebar.number_input("Number of questions to generate (max 50):", min_value=1, max_value=50, value=10, step=1)
elif mode == "Flashcards":
num_flashcards = st.sidebar.number_input("Number of flashcards to generate (max 20):", min_value=1, max_value=20, value=5, step=1)
# ---------------------------
# Session State Initialization
# ---------------------------
if "pdf_text" not in st.session_state:
st.session_state.pdf_text = None
if "summary" not in st.session_state:
st.session_state.summary = None
if "chat_history" not in st.session_state:
st.session_state.chat_history = [{"role": "assistant", "content": "Hi, how can I help you with your study material?"}]
if "questions_table" not in st.session_state:
st.session_state.questions_table = None
if "flashcards" not in st.session_state:
st.session_state.flashcards = {}
if "current_card" not in st.session_state:
st.session_state.current_card = 0
if "score" not in st.session_state:
st.session_state.score = 0
if "show_answer" not in st.session_state:
st.session_state.show_answer = False
if "flashcard_keys" not in st.session_state:
st.session_state.flashcard_keys = []
if "user_answers" not in st.session_state:
st.session_state.user_answers = {}
if "shuffle_cards" not in st.session_state:
st.session_state.shuffle_cards = False
# Video-related session states
if "video_transcript" not in st.session_state:
st.session_state.video_transcript = None
if "video_chat_history" not in st.session_state:
st.session_state.video_chat_history = [{"role": "assistant", "content": "Hi, how can I help you with the video content?"}]
if "processed_video_path" not in st.session_state:
st.session_state.processed_video_path = None
if "video_summary" not in st.session_state:
st.session_state.video_summary = None
# ---------------------------
# Process PDF Upload
# ---------------------------
if uploaded_pdf is not None:
st.session_state.pdf_text = extract_text(uploaded_pdf)
if st.session_state.pdf_text:
st.sidebar.success("PDF uploaded and processed successfully!")
else:
st.sidebar.error("Failed to extract text. Please check your PDF file.")
# ---------------------------
# Process Video Upload
# ---------------------------
if uploaded_video is not None:
# Generate a unique identifier for the video
video_id = f"{uploaded_video.name}_{uploaded_video.size}"
# Only process if it's a new video
if st.session_state.processed_video_path != video_id:
transcript, video_path = process_uploaded_video(uploaded_video)
if transcript:
st.session_state.video_transcript = transcript
st.session_state.processed_video_path = video_id
st.session_state.video_summary = None # Reset summary for new video
st.sidebar.success("✅ Video processed and transcribed successfully!")
else:
st.sidebar.error("Failed to process video.")
else:
st.sidebar.info("Using cached video transcript.")
# ---------------------------
# Main Area: Mode-Based Display (all functions via side menu)
# ---------------------------
# Page configuration and custom styling
st.markdown("""
""", unsafe_allow_html=True)
st.markdown("""
📚 Study Companion
Your AI-powered learning assistant
""", unsafe_allow_html=True)
# ---------------------------
# Main Display - Handle PDF and Video inputs
# ---------------------------
if input_source == "PDF Document":
if st.session_state.pdf_text is None:
st.info("Please upload a PDF from the sidebar to begin.")
else:
if mode == "Chat":
st.header("💬 Chat with Your Study Companion")
# Display persistent chat history
for msg in st.session_state.chat_history:
st.chat_message(msg["role"]).write(msg["content"])
user_question = st.chat_input("💭 Ask a question about the document...")
if user_question:
st.session_state.chat_history.append({"role": "user", "content": user_question})
st.chat_message("user").write(user_question)
with st.spinner("🤔 Thinking..."):
response = chat_with_document(st.session_state.pdf_text, st.session_state.chat_history, user_question)
st.session_state.chat_history.append({"role": "assistant", "content": response})
st.chat_message("assistant").write(response)
# Add a clear chat button
if len(st.session_state.chat_history) > 1:
if st.button("🗑️ Clear Chat History"):
st.session_state.chat_history = [{"role": "assistant", "content": "Hi, how can I help you with your study material?"}]
st.rerun()
elif mode == "Test Your Knowledge":
st.header("📝 Test Your Knowledge")
if num_questions is None:
st.info("Please specify the number of questions in the sidebar.")
else:
if st.button("📋 Generate Questions", use_container_width=True, type="primary"):
with st.spinner("✨ Generating questions..."):
questions_output = generate_questions_from_text(st.session_state.pdf_text, num_questions)
st.session_state.questions_table = questions_output
if st.session_state.questions_table:
st.markdown(st.session_state.questions_table)
# Download button for questions
st.download_button(
label="📥 Download Questions",
data=st.session_state.questions_table,
file_name="study_questions.md",
mime="text/markdown"
)
else:
st.info("💡 Click 'Generate Questions' to create a quiz based on your document.")
elif mode == "Flashcards":
st.header("🎴 Interactive Flashcards")
# Custom CSS for flashcard styling with flip effect
st.markdown("""
""", unsafe_allow_html=True)
# Sidebar controls for flashcards
with st.sidebar:
st.markdown("---")
st.subheader("🎴 Flashcard Options")
shuffle_option = st.checkbox("Shuffle cards", value=st.session_state.shuffle_cards)
if shuffle_option != st.session_state.shuffle_cards:
st.session_state.shuffle_cards = shuffle_option
if st.session_state.flashcards and st.session_state.flashcard_keys:
if shuffle_option:
st.session_state.flashcard_keys = list(st.session_state.flashcards.keys())
random.shuffle(st.session_state.flashcard_keys)
else:
st.session_state.flashcard_keys = list(st.session_state.flashcards.keys())
if num_flashcards is None:
#st.info("Please specify the number of flashcards in the sidebar.")
pass
else:
col1, col2 = st.columns([3, 1])
with col1:
if st.button("🎯 Generate Flashcards", use_container_width=True):
with st.spinner("✨ Creating your flashcards..."):
flashcards = generate_flashcards_from_text(st.session_state.pdf_text, num_flashcards)
if flashcards:
st.session_state.flashcards = flashcards
st.session_state.flashcard_keys = list(flashcards.keys())
if st.session_state.shuffle_cards:
random.shuffle(st.session_state.flashcard_keys)
st.session_state.current_card = 0
st.session_state.score = 0
st.session_state.show_answer = False
st.session_state.user_answers = {}
st.success("✅ Flashcards generated successfully!")
st.rerun()
else:
st.error("Failed to generate flashcards. Please try again.")
with col2:
if st.session_state.flashcards and st.button("🔄 Reset", use_container_width=True):
st.session_state.current_card = 0
st.session_state.score = 0
st.session_state.show_answer = False
st.session_state.user_answers = {}
st.rerun()
if not st.session_state.flashcards:
st.info("💡 No flashcards available. Click 'Generate Flashcards' to create a study set based on your document.")
else:
total_cards = len(st.session_state.flashcards)
if st.session_state.current_card >= total_cards:
# Completion screen
st.balloons()
st.markdown(f"""
🎉 Congratulations! You've completed all flashcards!
Final Score: {st.session_state.score} / {total_cards} ({int(st.session_state.score/total_cards*100)}%)
""", unsafe_allow_html=True)
# Show review of incorrect answers
if st.session_state.user_answers:
st.subheader("📝 Review Your Answers")
for i, (q, is_correct) in enumerate(st.session_state.user_answers.items(), 1):
if not is_correct:
with st.expander(f"❌ Card {i}: {q[:50]}..."):
st.write(f"**Question:** {q}")
st.write(f"**Answer:** {st.session_state.flashcards[q]}")
col1, col2 = st.columns(2)
with col1:
if st.button("🔄 Practice Again", use_container_width=True):
st.session_state.current_card = 0
st.session_state.score = 0
st.session_state.show_answer = False
st.session_state.user_answers = {}
if st.session_state.shuffle_cards:
random.shuffle(st.session_state.flashcard_keys)
st.rerun()
with col2:
if st.button("📚 New Set", use_container_width=True):
st.session_state.flashcards = {}
st.session_state.flashcard_keys = []
st.session_state.current_card = 0
st.session_state.score = 0
st.session_state.show_answer = False
st.session_state.user_answers = {}
st.rerun()
else:
# Progress bar with percentage
progress = (st.session_state.current_card / total_cards) * 100
st.markdown(f"""
""", unsafe_allow_html=True)
# Display current card with flip effect
current_key = st.session_state.flashcard_keys[st.session_state.current_card]
current_answer = st.session_state.flashcards[current_key]
# Flip card display
flip_class = "flipped" if st.session_state.show_answer else ""
st.markdown(f"""
Card {st.session_state.current_card + 1} of {total_cards}
{current_key}
👆 Click below to flip
Card {st.session_state.current_card + 1} of {total_cards}
Answer:
{current_answer}
""", unsafe_allow_html=True)
# Flip button
col_flip1, col_flip2, col_flip3 = st.columns([1, 2, 1])
with col_flip2:
if st.button("🔄 Flip Card", use_container_width=True, key="flip_btn"):
st.session_state.show_answer = not st.session_state.show_answer
st.rerun()
# Navigation buttons (Previous/Next)
nav_col1, nav_col2, nav_col3 = st.columns([1, 1, 1])
with nav_col1:
if st.button("⬅️ Previous", use_container_width=True, key="prev_btn",
disabled=(st.session_state.current_card == 0)):
st.session_state.current_card -= 1
st.session_state.show_answer = False
st.rerun()
with nav_col2:
# Show current position
st.markdown(f"""
{st.session_state.current_card + 1} / {total_cards}
""", unsafe_allow_html=True)
with nav_col3:
if st.button("Next ➡️", use_container_width=True, key="next_btn",
disabled=(st.session_state.current_card >= total_cards - 1)):
st.session_state.current_card += 1
st.session_state.show_answer = False
st.rerun()
# Self-assessment buttons (only when answer is shown)
if st.session_state.show_answer:
st.markdown("### Did you get it right?")
assess_col1, assess_col2, assess_col3 = st.columns([1, 1, 1])
with assess_col1:
if st.button("✅ Got it!", use_container_width=True, type="primary", key="correct_btn"):
st.session_state.score += 1
st.session_state.user_answers[current_key] = True
if st.session_state.current_card < total_cards - 1:
st.session_state.current_card += 1
st.session_state.show_answer = False
else:
st.session_state.current_card += 1
st.rerun()
with assess_col2:
if st.button("❌ Missed it", use_container_width=True, key="wrong_btn"):
st.session_state.user_answers[current_key] = False
if st.session_state.current_card < total_cards - 1:
st.session_state.current_card += 1
st.session_state.show_answer = False
else:
st.session_state.current_card += 1
st.rerun()
with assess_col3:
if st.button("⏭️ Skip", use_container_width=True, key="skip_btn"):
if st.session_state.current_card < total_cards - 1:
st.session_state.current_card += 1
st.session_state.show_answer = False
else:
st.session_state.current_card += 1
st.rerun()
# Current score display
st.markdown(f"""
Current Score: {st.session_state.score} / {st.session_state.current_card}
{f"({int(st.session_state.score/st.session_state.current_card*100)}%)" if st.session_state.current_card > 0 else ""}
""", unsafe_allow_html=True)
# ---------------------------
# Video Input Display
# ---------------------------
elif input_source == "Video":
if st.session_state.video_transcript is None:
st.info("🎥 Please upload a video from the sidebar to begin.")
else:
if mode == "Transcript":
st.header("📄 Video Transcript")
st.write("Below is the complete transcript of the video:")
st.text_area("Transcript", st.session_state.video_transcript, height=400, disabled=True, label_visibility="collapsed")
# Add download button for transcript
st.download_button(
label="📥 Download Transcript",
data=st.session_state.video_transcript,
file_name="video_transcript.txt",
mime="text/plain"
)
elif mode == "Summary":
st.header("📝 Video Summary & Key Points")
# Generate summary only once and cache it
if st.session_state.video_summary is None:
with st.spinner("✨ Generating summary..."):
summary = generate_video_summary(st.session_state.video_transcript)
st.session_state.video_summary = summary
st.markdown(st.session_state.video_summary)
# Download button for summary
st.download_button(
label="📥 Download Summary",
data=st.session_state.video_summary,
file_name="video_summary.md",
mime="text/markdown"
)
elif mode == "Chat":
st.header("💬 Chat About the Video")
# Display persistent chat history for video
for msg in st.session_state.video_chat_history:
st.chat_message(msg["role"]).write(msg["content"])
user_question = st.chat_input("💭 Ask a question about the video...")
if user_question:
st.session_state.video_chat_history.append({"role": "user", "content": user_question})
st.chat_message("user").write(user_question)
with st.spinner("🤔 Thinking..."):
response = chat_with_video(
st.session_state.video_transcript,
st.session_state.video_chat_history,
user_question
)
st.session_state.video_chat_history.append({"role": "assistant", "content": response})
st.chat_message("assistant").write(response)
# Add a clear chat button
if len(st.session_state.video_chat_history) > 1:
if st.button("🗑️ Clear Chat History", key="clear_video_chat"):
st.session_state.video_chat_history = [{"role": "assistant", "content": "Hi, how can I help you with the video content?"}]
st.rerun()