# app.py

import streamlit as st
from components.todo import ToDo
from components.emotion import EmotionDetector
from components.llm import OllamaLLM
from components.study_material import StudyMaterialProcessor
from components.qa_generator import QAGenerator
from logger import logger

@st.cache_resource
def load_llm():
    logger.debug("Loading OllamaLLM and caching the instance.")
    return OllamaLLM(model_name="llama2", host="http://localhost:11434")

def main():
    logger.debug("Starting the Visual Study Buddy application.")
    st.set_page_config(page_title="📚 Visual Study Buddy", layout="wide")
    st.title("📚 Visual Study Buddy")

    # Initialize Components
    todolist = ToDo()
    logger.debug("Initialized ToDo component.")
    emotion_detector = EmotionDetector()
    logger.debug("Initialized EmotionDetector component.")
    processor = StudyMaterialProcessor()
    logger.debug("Initialized StudyMaterialProcessor component.")
    qa_generator = QAGenerator()
    logger.debug("Initialized QAGenerator component.")

    # Initialize LLM and store in session_state
    if 'llm' not in st.session_state:
        st.session_state.llm = load_llm()
        logger.info("Loaded and cached OllamaLLM.")
    else:
        logger.debug("Loaded existing OllamaLLM from session_state.")

    # Sidebar for To-Do List
    st.sidebar.header("📝 To-Do List")
    with st.sidebar.form("add_task_form"):
        new_task = st.text_input("Add a new task")
        submit_task = st.form_submit_button("Add Task")
        if submit_task:
            logger.info(f"User submitted a new task: {new_task}")
            todolist.add_task(new_task)

    # Display To-Do List
    st.sidebar.subheader("Your Tasks")
    todolist.display_tasks()

    # Main Content Layout
    col1, col2 = st.columns(2)

    with col1:
        logger.debug("Rendering EmotionDetector in main content.")
        emotion_detector.display_emotion()
        
        st.markdown("---")
        st.header("📂 Add Study Materials")
        input_type = st.selectbox("Select input type:", ["Text", "URL", "PDF"], key="select_input_type")
        
        if input_type == "Text":
            user_text = st.text_area("Enter your study material here:", key="text_input")
            if st.button("Process Text", key="process_text"):
                logger.info("Processing Text input.")
                index = processor.process_input("Text", user_text)
                if index:
                    st.session_state.study_index = index
                    logger.info("Text input processed and index stored in session_state.")
                    st.success("Text processed and index created.")
        
        elif input_type == "URL":
            user_url = st.text_input("Enter the URL of the study material:", key="url_input")
            if st.button("Process URL", key="process_url"):
                logger.info(f"Processing URL input: {user_url}.")
                index = processor.process_input("URL", user_url)
                if index:
                    st.session_state.study_index = index
                    logger.info("URL input processed and index stored in session_state.")
                    st.success("URL content fetched and index created.")
        
        elif input_type == "PDF":
            uploaded_pdf = st.file_uploader("Upload a PDF file", type=["pdf"], key="pdf_uploader")
            if uploaded_pdf and st.button("Process PDF", key="process_pdf"):
                logger.info("Processing PDF input.")
                index = processor.process_input("PDF", uploaded_pdf)
                if index:
                    st.session_state.study_index = index
                    logger.info("PDF input processed and index stored in session_state.")
                    st.success("PDF processed and index created.")

    with col2:
        logger.debug("Rendering QAGenerator in main content.")
        st.header("❓ Q&A Generator")
        user_question = st.text_input("Ask a question related to your study material:", key="qa_input")
        if st.button("Get Answer", key="get_answer"):
            logger.info(f"User asked a question: {user_question}")
            if 'study_index' in st.session_state and st.session_state.study_index:
                answer = qa_generator.create_response(st.session_state.study_index, user_question)
                logger.info(f"Generated answer: {answer}")
                st.write(f"**Answer:** {answer}")
            else:
                logger.warning("User attempted to get an answer without processing study materials.")
                st.error("Please add and process study materials first.")

    # Display Current Task
    st.markdown("---")
    if not st.session_state.todo.empty:
        current_task = st.session_state.todo.iloc[0]['Task']
        st.subheader(f"📌 Current Task: {current_task}")
        logger.debug(f"Displaying current task: {current_task}")
    else:
        st.subheader("📌 No current task.")
        logger.debug("No current task to display.")

    # Display Emotion-based Message at the Top
    st.markdown("""---""")
    st.subheader(f"😊 Current Emotion: {st.session_state.emotion}")
    logger.debug(f"Displaying current emotion: {st.session_state.emotion}")

    # Footer
    st.markdown("""---""")
    st.markdown("Developed by [Your Name](https://github.com/yourgithub)")
    logger.debug("Rendered footer.")

if __name__ == "__main__":
    main()
    logger.debug("Visual Study Buddy application has terminated.")

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

# components/emotion.py

import streamlit as st
from fer import FER
from PIL import Image
import numpy as np
from streamlit_webrtc import WebRtcMode, webrtc_streamer, VideoTransformerBase
from database_mongodb import EmotionDatabase
from logger import logger
from threading import Lock
from datetime import datetime, timedelta

class EmotionDetector:
    def __init__(self):
        logger.debug("Initializing EmotionDetector component.")
        self.db = EmotionDatabase()
        self.lock = Lock()
        if 'detector' not in st.session_state:
            try:
                st.session_state.detector = FER(mtcnn=True)
                logger.info("Initialized and cached FER detector in session state.")
            except Exception as e:
                logger.error(f"Failed to initialize FER detector: {e}")
                st.error(f"Failed to initialize FER detector: {e}")
        
        if 'emotion' not in st.session_state:
            st.session_state.emotion = 'Neutral'
            logger.info("Set initial emotion to Neutral in session state.")
        else:
            logger.info(f"Loaded existing emotion '{st.session_state.emotion}' from session state.")

    def display_emotion(self):
        st.subheader("🎥 Real-Time Emotion Detection")
        
        class EmotionTransformer(VideoTransformerBase):
            def __init__(self, emotion_detector):
                self.emotion_detector = emotion_detector
                self.db = emotion_detector.db
                self.lock = emotion_detector.lock
                self.emotion = 'Neutral'
                logger.debug("EmotionTransformer initialized.")
            
            def transform(self, frame):
                logger.debug("Transform method called.")
                try:
                    img = frame.to_image()
                    emotion = self.emotion_detector.detect_emotion(img)
                    
                    logger.debug(f"Detected emotion: {emotion}")
                    with self.lock:
                        self.emotion = emotion
                        self.db.insert_emotion(emotion)
                    
                    # Annotate the image with detected emotion
                    annotated_img = self.emotion_detector.annotate_image(img, emotion)
                    return annotated_img
                except Exception as e:
                    logger.error(f"Error in transform method: {e}")
                    return frame.to_image()  # Return unmodified frame on error
                    
        webrtc_ctx = webrtc_streamer(
            key="emotion-detection",
            mode=WebRtcMode.SENDRECV,
            video_transformer_factory=lambda: EmotionTransformer(self),
            media_stream_constraints={"video": True, "audio": False},
            async_processing=True,
        )
        
        # Analyze emotions periodically to determine persistent mood
        with st.spinner("Analyzing emotions..."):
            emotion = st.session_state.get('emotion', 'Neutral')
            st.write(f"**Current Emotion:** {emotion}")
            self.evaluate_emotion(emotion)

    def detect_emotion(self, image: Image.Image) -> str:
        """
        Detect emotion from the given image.

        :param image: PIL Image.
        :return: Detected emotion as a string.
        """
        logger.debug("Resizing image for faster emotion detection.")
        image = image.resize((320, 240))  # Reduce resolution for speed
        img_array = np.array(image.convert('RGB'))
        try:
            emotions = st.session_state.detector.top_emotion(img_array)
            if emotions:
                emotion, score = emotions
                logger.info(f"Detected emotion: {emotion} with score {score:.2f}.")
                return emotion.capitalize()
            logger.info("No emotion detected; defaulting to Neutral.")
            return 'Neutral'
        except Exception as e:
            logger.error(f"Error during emotion detection: {e}")
            return 'Neutral'
    
    def annotate_image(self, image: Image.Image, emotion: str) -> Image.Image:
        """
        Annotate the image with the detected emotion.

        :param image: PIL Image.
        :param emotion: Detected emotion.
        :return: Annotated PIL Image.
        """
        from PIL import ImageDraw, ImageFont
        
        draw = ImageDraw.Draw(image)
        try:
            font = ImageFont.truetype("arial.ttf", 24)
        except IOError:
            font = ImageFont.load_default()
        
        text = f"Emotion: {emotion}"
        draw.text((10, 10), text, fill="red", font=font)
        return image
    
    def evaluate_emotion(self, current_emotion: str):
        """
        Evaluate the current emotion compared to recent emotions and display messages accordingly.

        :param current_emotion: The latest detected emotion.
        """
        # Define the time window for analysis (e.g., last 5 minutes)
        time_window_minutes = 5
        recent_emotions = self.db.get_recent_emotions(minutes=time_window_minutes)
        emotion_counts = {}
        for emotion in recent_emotions:
            emotion_counts[emotion] = emotion_counts.get(emotion, 0) + 1
        
        logger.debug(f"Emotion counts in the last {time_window_minutes} minutes: {emotion_counts}")
        
        if not recent_emotions:
            st.info("No recent emotions detected.")
            logger.info("No recent emotions to analyze.")
            return
        
        # Determine the most frequent emotion
        most_frequent_emotion = max(emotion_counts, key=emotion_counts.get)
        frequency = emotion_counts[most_frequent_emotion]
        total = len(recent_emotions)
        percentage = (frequency / total) * 100
        
        logger.debug(f"Most frequent emotion: {most_frequent_emotion} ({percentage:.2f}%)")
        
        # Set a threshold for considering an emotion as persistent
        persistence_threshold = 70  # Percent
        
        if percentage > persistence_threshold:
            if most_frequent_emotion in ['Happy', 'Surprise']:
                st.success("You're feeling great! Keep up the positive vibes! 😊")
                logger.info("Displayed Happy/Surprise feedback.")
            elif most_frequent_emotion in ['Sad', 'Angry']:
                st.warning("It seems you're feeling down. Consider taking a short break or talking to someone. 😢")
                logger.info("Displayed Sad/Angry feedback.")
            else:
                st.info("You're in a neutral mood. Keep focused and continue your study session! 📖")
                logger.info("Displayed Neutral/Other feedback.")
        else:
            st.info("Your mood varies. Stay consistent and take care of yourself! 🌟")
            logger.info("Displayed Varying mood feedback.")



# 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."

# components/qa_generator.py

import streamlit as st
from llama_index import SimpleVectorIndex
from logger import logger

class QAGenerator:
    def __init__(self):
        logger.debug("Initializing QAGenerator component.")
    
    def create_response(self, index: SimpleVectorIndex, query: str) -> str:
        """
        Query the index with the user's question and get the response from LLM.
        
        :param index: The SimpleVectorIndex instance.
        :param query: The user's question.
        :return: The answer string.
        """
        if not index:
            st.error("No index available. Please add and process study materials first.")
            logger.error("Attempted to generate a response without a valid index.")
            return ""
        try:
            logger.debug(f"Querying index with question: {query}")
            response = index.query(query, llm=st.session_state.llm)
            answer = response.response
            logger.info(f"Generated answer for query '{query}': {answer}")
            return answer
        except Exception as e:
            logger.error(f"Error generating response for query '{query}': {e}")
            st.error("An error occurred while generating the response.")
            return "I'm sorry, I couldn't generate a response."


import requests
import json

class OllamaEmbeddingModel:
    def __init__(self):
        self.base_url = "http://localhost:11434/api/embeddings"  # Updated URL
    
    def get_embeddings(self, text: str):
        """
        Get embeddings for the provided text from Ollama.
        
        :param text: The input text to be converted to embeddings.
        :return: A list of embeddings if successful, else an empty list.
        """
        payload = {
            "model": "nomic-embed-text",  # Ensure this model is available in Ollama
            "prompt": text
        }

        try:
            response = requests.post(self.base_url, json=payload)
            response.raise_for_status()  
            
            data = response.json()
            embeddings = data.get('embeddings', [])
            return embeddings
        except requests.exceptions.RequestException as e:
            print(f"Error fetching embeddings from Ollama: {e}")
            return []

# Example test to fetch embeddings for a sample sentence
if __name__ == "__main__":
    embedding_model = OllamaEmbeddingModel()
    text = "This is a test sentence to get embeddings."
    embeddings = embedding_model.get_embeddings(text)
    
    if embeddings:
        print("Embeddings:", embeddings)
    else:
        print("No embeddings returned.")

import streamlit as st
import requests
from bs4 import BeautifulSoup
from llama_index.core import VectorStoreIndex, Document
from components.ollama_embedding import OllamaEmbeddingModel
import PyPDF2
import io
from logger import logger

class StudyMaterialProcessor:
    def __init__(self):
        logger.debug("Initializing StudyMaterialProcessor component.")
        self.embedding_model = OllamaEmbeddingModel()
    
    def process_input(self, input_type: str, input_data) -> VectorStoreIndex:
        logger.debug(f"Processing input of type: {input_type}.")
        documents = []

        if input_type == "Text":
            logger.debug("Processing Text input.")
            documents = [Document(text=input_data)]
        elif input_type == "URL":
            logger.debug(f"Processing URL input: {input_data}.")
            try:
                response = requests.get(input_data, timeout=10)
                response.raise_for_status()
                soup = BeautifulSoup(response.text, 'html.parser')
                text = soup.get_text(separator='\n')
                documents.append(Document(text=text))
                logger.info(f"Fetched and extracted text from URL: {input_data}.")
            except Exception as e:
                logger.error(f"Error fetching URL '{input_data}': {e}")
                st.error(f"Error fetching URL: {e}")
                return None
        elif input_type == "PDF":
            logger.debug("Processing PDF input.")
            try:
                pdf_reader = PyPDF2.PdfReader(io.BytesIO(input_data.read()))
                for page in pdf_reader.pages:
                    text = page.extract_text()
                    if text:
                        documents.append(Document(text=text))
                logger.info("Extracted text from PDF.")
            except Exception as e:
                logger.error(f"Error reading PDF: {e}")
                st.error(f"Error reading PDF: {e}")
                return None
        else:
            logger.warning(f"Unsupported input type: {input_type}.")
            st.error("Unsupported input type.")
            return None

        if documents:
            try:
                with st.spinner("Creating index. Please wait..."):
                    index = VectorStoreIndex.from_documents(documents, embed_model=self.embedding_model)
                    logger.info("Index created successfully.")
                    return index
            except Exception as e:
                logger.error(f"Error creating index: {e}")
                st.error(f"Error creating index: {e}")
                logger.debug("Exception details:", exc_info=True)
                return None
        else:
            logger.warning("No valid documents to index.")
            st.error("No valid documents to index.")
            return None


# 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' not in st.session_state:
            st.session_state.todo = 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['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 = pd.concat([st.session_state.todo, 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 toggle_task(self, index: int):
        try:
            current_status = st.session_state.todo.at[index, 'Status']
            new_status = 'Completed' if current_status == 'Pending' else 'Pending'
            st.session_state.todo.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):
        logger.debug("Displaying tasks.")
        st.subheader("Your Tasks")
        if not st.session_state.todo.empty:
            for index, row in st.session_state.todo.iterrows():
                checkbox = st.checkbox(row['Task'], value=(row['Status'] == 'Completed'), key=f"task_{index}")
                if checkbox != (row['Status'] == 'Completed'):
                    self.toggle_task(index)
        else:
            st.info("No tasks added yet.")
            logger.info("To-do list is currently empty.")


what job does qa generator and study material do
do they form a rag model?