# components/todo.py

import pandas as pd
import streamlit as st
from logger import logger

class ToDo:
    def __init__(self):
        logger.debug("Initializing ToDo component.")
        if 'todo_list' not in st.session_state:
            st.session_state.todo_list = pd.DataFrame(columns=['Task', 'Status'])
            logger.info("Created new to-do list in session state.")
        else:
            logger.info("Loaded existing to-do list from session state.")
    
    def add_task(self, task: str):
        if task:
            if task in st.session_state.todo_list['Task'].values:
                logger.warning(f"Attempted to add duplicate task: {task}")
                st.warning(f"Task '{task}' already exists.")
                return
            new_task_df = pd.DataFrame([{'Task': task, 'Status': 'Pending'}])
            st.session_state.todo_list = pd.concat([st.session_state.todo_list, new_task_df], ignore_index=True)
            logger.info(f"Added new task: {task}")
            st.success(f"Added task: {task}")
        else:
            logger.warning("Attempted to add empty task.")
            st.warning("Task cannot be empty.")
    
    def get_tasks(self):
        """Return list of all tasks"""
        return st.session_state.todo_list['Task'].tolist()
    
    def toggle_task(self, index: int):
        try:
            current_status = st.session_state.todo_list.at[index, 'Status']
            new_status = 'Completed' if current_status == 'Pending' else 'Pending'
            st.session_state.todo_list.at[index, 'Status'] = new_status
            logger.info(f"Toggled task at index {index} to {new_status}.")
        except Exception as e:
            logger.error(f"Error toggling task at index {index}: {e}")
            st.error("An error occurred while toggling the task status.")
    
    def display_tasks(self):
        """Display tasks and return True if there are tasks, False otherwise"""
        logger.debug("Displaying tasks.")
        st.subheader("Your Tasks")
        if not st.session_state.todo_list.empty:
            for index, row in st.session_state.todo_list.iterrows():
                checkbox = st.checkbox(row['Task'], value=(row['Status'] == 'Completed'), key=f"task_{index}")
                if checkbox != (row['Status'] == 'Completed'):
                    self.toggle_task(index)
            return True
        else:
            st.info("No tasks added yet.")
            logger.info("To-do list is currently empty.")
            return False

# components/qa_generator.py

import streamlit as st
from langchain_huggingface import HuggingFaceEndpoint, HuggingFaceEmbeddings
from langchain_community.document_loaders import PyPDFLoader, TextLoader, WebBaseLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain.prompts import PromptTemplate
from langchain.chains import RetrievalQA
from langchain.docstore.document import Document
from logger import logger
import chromadb
import os

class QAGenerator:
    def __init__(self):
        logger.debug("Initializing QAGenerator with LangChain and HuggingFace.")
        
        # Use HuggingFace API token from environment or secrets
        self.hf_token = st.secrets.get("hf_api_key") or os.getenv("HUGGINGFACE_API_KEY")
        if not self.hf_token:
            raise ValueError("HuggingFace API token not found in environment or secrets")
        
        # Simplified LLM configuration using Mistral
        self.llm = HuggingFaceEndpoint(
            repo_id="mistralai/Mistral-7B-Instruct-v0.3",
            max_length=128,
            temperature=0.7,
            token=self.hf_token
        )
        
        # Initialize embeddings
        self.embeddings = HuggingFaceEmbeddings(
            model_name="sentence-transformers/all-MiniLM-L6-v2",
            model_kwargs={'device': 'cpu'},
            encode_kwargs={'normalize_embeddings': True}
        )
        
        self.retriever = None
        self.db = None
        self.chroma_client = chromadb.Client()
    
    def process_documents(self, input_type: str, input_data) -> bool:
        try:
            if input_type == "PDF":
                loader = PyPDFLoader(input_data)
                documents = loader.load()
            elif input_type == "URL":
                loader = WebBaseLoader(input_data)
                documents = loader.load()
            elif input_type == "Text":
                documents = [Document(page_content=input_data, metadata={"source": "user_input"})]
            else:
                st.error("Unsupported input type.")
                return False

            text_splitter = RecursiveCharacterTextSplitter(
                chunk_size=500,
                chunk_overlap=50
            )
            docs = text_splitter.split_documents(documents)
            logger.info(f"Processed and split documents into {len(docs)} chunks.")

            # Clear existing collection if it exists
            try:
                self.chroma_client.delete_collection("study_materials")
            except:
                pass

            self.db = Chroma.from_documents(
                documents=docs,
                embedding=self.embeddings,
                client=self.chroma_client,
                collection_name="study_materials"
            )
            
            # Adjust retriever to match number of available documents
            k = min(2, len(docs))
            self.retriever = self.db.as_retriever(search_kwargs={"k": k})
            logger.info("Index created and retriever is ready.")
            return True
            
        except Exception as e:
            logger.error(f"Error creating index from {input_type}: {e}")
            st.error(f"Failed to create index from the provided input: {str(e)}")
            return False

    def create_response(self, query: str) -> str:
        if not self.retriever:
            st.error("No index available. Please add and process study materials first.")
            return ""
        
        try:
            prompt_template = """Based on the following context, provide a detailed and accurate answer to the question. If the context doesn't contain enough information, say so.

Context: {context}

Question: {question}

Answer:"""
            
            PROMPT = PromptTemplate(
                template=prompt_template,
                input_variables=["context", "question"]
            )
            
            qa_chain = RetrievalQA.from_chain_type(
                llm=self.llm,
                chain_type="stuff",
                retriever=self.retriever,
                chain_type_kwargs={"prompt": PROMPT}
            )
            
            try:
                response = qa_chain.invoke({"query": query})
                answer = response.get("result", "").strip()
                if not answer:
                    answer = "I couldn't generate a specific answer based on the context."
            except Exception as e:
                logger.error(f"Error in QA chain: {str(e)}")
                answer = "I encountered an error while processing your question. Please try again."
            
            logger.info(f"Generated answer for query '{query}': {answer}")
            return answer
            
        except Exception as e:
            error_msg = f"Error generating response for query '{query}': {str(e)}"
            logger.error(error_msg)
            st.error("An error occurred while generating the response.")
            return "I'm sorry, I couldn't generate a response due to an error."

# components/llm.py

import requests
import streamlit as st
from langchain.llms.base import LLM
from pydantic import Field
from typing import Optional
from logger import logger

class OllamaLLM(LLM):
    """
    A custom LangChain LLM wrapper for Ollama's local API.
    """
    model_name: str = Field(default="llama3.1:latest", description="Name of the Ollama model to use")
    host: str = Field(default="http://localhost:11434", description="Host URL where Ollama's API is running")

    @property
    def _llm_type(self):
        return "ollama"
    
    def __init__(self, model_name: str = "llama3.1:latest", host: str = "http://localhost:11434", **kwargs):
        """
        Initialize the OllamaLLM with the specified model and host.
        
        :param model_name: Name of the Ollama model to use.
        :param host: Host URL where Ollama's API is running.
        """
        super().__init__(**kwargs)
        self.model_name = model_name
        self.host = host.rstrip('/')
        logger.debug(f"Initialized OllamaLLM with model '{self.model_name}' at host '{self.host}'.")
    
    def _call(self, prompt: str, stop: Optional[list] = None) -> str:
        """
        Send the prompt to Ollama's API and retrieve the response.
        
        :param prompt: The prompt string to send to the model.
        :param stop: Optional list of stop sequences.
        :return: The response string from the model.
        """
        url = f"{self.host}/v1/completions"
        headers = {
            "Content-Type": "application/json"
        }
        data = {
            "model": self.model_name,
            "prompt": prompt,
            "max_tokens": 256,  # Reduced for speed
            "temperature": 0.5,  # More deterministic outputs
            "top_p": 0.9,
            "stop": stop
        }

        logger.debug(f"Sending prompt to Ollama: {prompt}")
        try:
            response = requests.post(url, headers=headers, json=data)
            response.raise_for_status()
            result = response.json()
            completion = result["choices"][0]["text"].strip()
            logger.info(f"Received response from Ollama: {completion}")
            return completion
        except requests.exceptions.RequestException as e:
            logger.error(f"Error communicating with Ollama API: {e}")
            st.error("Error communicating with Ollama API.")
            return "I'm sorry, I couldn't process that."

# database_mongodb.py

import pymongo
from pymongo import MongoClient
from datetime import datetime, timedelta
from logger import logger
import os
from dotenv import load_dotenv

# Load environment variables
load_dotenv()

class EmotionDatabase:
    def __init__(self, uri=os.getenv("MONGODB_URI"), db_name="study_buddy", collection_name="emotions"):
        if not uri:
            logger.error("MONGODB_URI is not set in environment variables.")
            self.client = None
            self.db = None
            self.collection = None
            return

        try:
            self.client = MongoClient(uri)
            self.db = self.client[db_name]
            self.collection = self.db[collection_name]
            logger.info("Connected to MongoDB successfully.")
        except Exception as e:
            logger.error(f"Failed to connect to MongoDB: {e}")
            self.client = None
            self.db = None
            self.collection = None

    def insert_emotion(self, emotion):
        """
        Insert a detected emotion with a timestamp into the MongoDB collection.
        
        :param emotion: Detected emotion as a string.
        """
        if self.collection is not None:
            try:
                emotion_record = {
                    "timestamp": datetime.utcnow(),
                    "emotion": emotion
                }
                self.collection.insert_one(emotion_record)
                logger.debug(f"Inserted emotion '{emotion}' into MongoDB.")
            except Exception as e:
                logger.error(f"Error inserting emotion into MongoDB: {e}")
        else:
            logger.error("MongoDB collection is not initialized.")

    def get_recent_emotions(self, minutes=5):
        """
        Retrieve emotions detected in the last 'minutes' minutes.
        
        :param minutes: Time window in minutes.
        :return: List of emotions.
        """
        if self.collection is not None:
            try:
                time_threshold = datetime.utcnow() - timedelta(minutes=minutes)
                emotions_cursor = self.collection.find({"timestamp": {"$gte": time_threshold}})
                emotions = [doc['emotion'] for doc in emotions_cursor]
                logger.debug(f"Retrieved {len(emotions)} emotions from the last {minutes} minutes.")
                return emotions
            except Exception as e:
                logger.error(f"Error fetching recent emotions from MongoDB: {e}")
                return []
        else:
            logger.error("MongoDB collection is not initialized.")
            return []

    def close_connection(self):
        """
        Close the MongoDB connection.
        """
        if self.client:
            self.client.close()
            logger.debug("Closed MongoDB connection.")

# logger.py

import logging
import os
from logging.handlers import RotatingFileHandler

# Create a logger
logger = logging.getLogger("study_buddy_logger")
logger.setLevel(logging.DEBUG)  # Capture all levels of logs

# Create a logs directory if it doesn't exist
if not os.path.exists("logs"):
    os.makedirs("logs")

# Create a file handler that logs debug and higher level messages
file_handler = RotatingFileHandler("logs/study_buddy.log", maxBytes=5*1024*1024, backupCount=2)
file_handler.setLevel(logging.DEBUG)

# Create console handler with a higher log level
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

# Create a logging format
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)

# Add the handlers to the logger
logger.addHandler(file_handler)
logger.addHandler(console_handler)

import streamlit as st
import time
from datetime import datetime
import cv2
import numpy as np
from deepface import DeepFace
from components.todo import ToDo
from components.qa_generator import QAGenerator
from components.llm import OllamaLLM
from database_mongodb import EmotionDatabase
from logger import logger
import streamlit.components.v1 as components
import base64

def init_session_state():
    if 'page' not in st.session_state:
        st.session_state.page = 'welcome'
    if 'current_emotion' not in st.session_state:
        st.session_state.current_emotion = None
    if 'qa_generator' not in st.session_state:
        st.session_state.qa_generator = QAGenerator()
    if 'todo' not in st.session_state:
        st.session_state.todo = ToDo()
    if 'emotion_db' not in st.session_state:
        st.session_state.emotion_db = EmotionDatabase()
    if 'tasks' not in st.session_state:
        st.session_state.tasks = []
    if 'llm' not in st.session_state:
        st.session_state.llm = OllamaLLM()
    if 'camera_on' not in st.session_state:
        st.session_state.camera_on = False

def set_page_config():
    st.set_page_config(
        page_title="Sidekick - Study Vibes, No Jive",
        page_icon="📚",
        layout="wide",
        initial_sidebar_state="expanded"
    )
    
    # Custom CSS (same as before)
    st.markdown("""
        <style>
        .main-title {
            text-align: center;
            font-size: 3em;
            color: #1E88E5;
            margin-bottom: 0;
            padding-bottom: 0;
        }
        .tagline {
            text-align: center;
            font-size: 1.5em;
            color: #64B5F6;
            margin-top: 0;
            padding-top: 0;
        }
        .stButton>button {
            width: 100%;
            border-radius: 20px;
            height: 3em;
        }
        .task-container {
            background-color: #f0f2f6;
            padding: 20px;
            border-radius: 10px;
            margin: 10px 0;
        }
        .chat-container {
            background-color: #ffffff;
            padding: 20px;
            border-radius: 10px;
            margin: 10px 0;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        </style>
    """, unsafe_allow_html=True)

def change_page(new_page):
    st.session_state.page = new_page
    st.rerun()

def welcome_page():
    st.markdown("<h1 class='main-title'>Sidekick</h1>", unsafe_allow_html=True)
    st.markdown("<p class='tagline'>Study Vibes, No Jive</p>", unsafe_allow_html=True)
    
    col1, col2, col3 = st.columns([1,2,1])
    with col2:
        st.markdown("""
            <div style='text-align: center; padding: 50px;'>
                <h2>👋 Hello! How are you today?</h2>
            </div>
        """, unsafe_allow_html=True)
        
        feeling = st.selectbox("I'm feeling...", 
            ["Select your mood", "Energetic 🚀", "Ready to learn 📚", "Tired 😴", "Anxious 😰", "Just okay 😊"])
        
        if feeling != "Select your mood":
            st.markdown("""
                <div style='text-align: center; padding: 20px;'>
                    <h3>Let's make today productive! 💪</h3>
                </div>
            """, unsafe_allow_html=True)
            if st.button("Start My Study Session"):
                change_page('goals')

def goals_page():
    st.markdown("<h1 class='main-title'>Sidekick</h1>", unsafe_allow_html=True)
    st.markdown("<p class='tagline'>Study Vibes, No Jive</p>", unsafe_allow_html=True)
    
    st.markdown("### 🎯 What do you want to achieve today?")
    
    col1, col2 = st.columns([2,1])
    
    with col1:
        new_task = st.text_input("Enter your study goal")
        if st.button("Add Goal"):
            if new_task:
                st.session_state.todo.add_task(new_task)
    
    with col2:
        if st.button("Let's Start Studying!", key="start_study"):
            change_page('study_prep')
    
    st.session_state.todo.display_tasks()

def generate_study_guide(task):
    prompt = f"""Create a comprehensive study guide for: {task}
    Include:
    1. Key topics to cover
    2. Suggested study approach
    3. Recommended resources
    4. Time management tips
    """
    return st.session_state.llm._call(prompt)

def study_prep_page():
    st.markdown("<h1 class='main-title'>Sidekick</h1>", unsafe_allow_html=True)
    st.markdown("<p class='tagline'>Study Vibes, No Jive</p>", unsafe_allow_html=True)
    
    st.markdown("### 📚 Let's prepare your study session")
    
    # Task selection and study guide generation
    tasks = st.session_state.todo.get_tasks()
    if tasks:
        selected_task = st.selectbox("Which task would you like to focus on?", tasks)
        if st.button("Generate Study Guide"):
            with st.spinner("Creating your personalized study guide..."):
                study_guide = generate_study_guide(selected_task)
                st.markdown("### 📋 Your Study Guide")
                st.write(study_guide)
        
        if st.button("Begin Study Session"):
            st.session_state.page = 'study_session'
            st.experimental_rerun()
    else:
        st.warning("Please add some study goals first!")
        if st.button("Back to Goals"):
            st.session_state.page = 'goals'
            st.experimental_rerun()

def capture_emotion():
    try:
        cap = cv2.VideoCapture(0)
        ret, frame = cap.read()
        cap.release()
        
        if ret:
            frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            result = DeepFace.analyze(frame_rgb, actions=['emotion'], enforce_detection=False)
            emotion = result[0]['dominant_emotion']
            st.session_state.emotion_db.insert_emotion(emotion)
            return emotion, frame_rgb
    except Exception as e:
        logger.error(f"Error in emotion capture: {e}")
    return None, None

def study_session_page():
    st.markdown("<h1 class='main-title'>Sidekick</h1>", unsafe_allow_html=True)
    
    # Create three columns for the main layout
    tasks_col, main_col, qa_col = st.columns([1,2,1])
    
    # Left column - Tasks and Emotion
    with tasks_col:
        st.markdown("### 📝 Tasks")
        st.session_state.todo.display_tasks()
        
        # Emotion tracking
        if st.button("Toggle Camera"):
            st.session_state.camera_on = not st.session_state.camera_on
        
        if st.session_state.camera_on:
            emotion, frame = capture_emotion()
            if emotion and frame is not None:
                st.image(frame, channels="RGB", use_column_width=True)
                st.write(f"Current emotion: {emotion}")
    
    # Middle column - Study Space and Chat
    with main_col:
        st.markdown("### 💭 Study Chat")
        user_message = st.text_input("Ask me anything about your studies...")
        if user_message:
            with st.spinner("Thinking..."):
                response = st.session_state.llm._call(user_message)
                st.markdown(f"**Assistant:** {response}")
    
    # Right column - Q&A Generator
    with qa_col:
        st.markdown("### 🤖 Course Assistant")
        
        input_type = st.selectbox("Select input type", ["Text", "PDF", "URL"])
        
        if input_type == "Text":
            user_input = st.text_area("Enter study material")
            if st.button("Process"):
                st.session_state.qa_generator.process_documents("Text", user_input)
        
        elif input_type == "PDF":
            uploaded_file = st.file_uploader("Upload PDF", type=['pdf'])
            if uploaded_file and st.button("Process"):
                st.session_state.qa_generator.process_documents("PDF", uploaded_file)
        
        elif input_type == "URL":
            url = st.text_input("Enter URL")
            if st.button("Process"):
                st.session_state.qa_generator.process_documents("URL", url)
        
        question = st.text_input("Ask a question about your material")
        if question and st.button("Get Answer"):
            answer = st.session_state.qa_generator.create_response(question)
            st.write("Answer:", answer)

def main():
    init_session_state()
    set_page_config()
    
    # Page routing
    if st.session_state.page == 'welcome':
        welcome_page()
    elif st.session_state.page == 'goals':
        goals_page()
    elif st.session_state.page == 'study_prep':
        study_prep_page()
    elif st.session_state.page == 'study_session':
        study_session_page()

if __name__ == "__main__":
    main()

while running the app.py
when i tapped begin study session
To effectively learn
2024-11-10 05:09:34.082 Uncaught app exception
Traceback (most recent call last):
  File "/Users/kartik/Desktop/vs/DevPost/study-buddy/env/lib/python3.11/site-packages/streamlit/runtime/scriptrunner/exec_code.py", line 88, in exec_func_with_error_handling
    result = func()
             ^^^^^^
  File "/Users/kartik/Desktop/vs/DevPost/study-buddy/env/lib/python3.11/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 579, in code_to_exec
    exec(code, module.__dict__)
  File "/Users/kartik/Desktop/vs/DevPost/study-buddy/app.py", line 249, in 
    main()
  File "/Users/kartik/Desktop/vs/DevPost/study-buddy/app.py", line 244, in main
    study_prep_page()
  File "/Users/kartik/Desktop/vs/DevPost/study-buddy/app.py", line 155, in study_prep_page
    st.experimental_rerun()
    ^^^^^^^^^^^^^^^^^^^^^
AttributeError: module 'streamlit' has no attribute 'experimental_rerun'