# Project export: Tailwind

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2025
- Tagline: Have you ever felt like your midterm or final review wasn't enough, tailwind has got you covered
- Devpost: https://devpost.com/software/tailwind
- GitHub: https://github.com/waterball01/cruzhacks25
- Video: https://www.youtube.com/embed/Hy-u4N3ZAVw?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — waterball01 (10 commits), ServeshKarnawat (4 commits)

## Devpost submission (written by the team)

### Inspiration

Have you ever spent hours if not days studying material for an exam only for the exam to be nothing like the study material. We have countless times and we wanted to come up with a solution to provide the perfect study material and extra practice directly based on the professors lectures.

### What it does

Tailwind creates study material directly based on professor's materials using RAG and AI to prepare students for exams. Students can submit their professors lecture notes, number of questions, and specific topics they want covered, and Tailwind creates practice problems from this information and then analyzes the answers to see how correct they are and gives feedback on the user's answers.

### How we built it

We made this using a flask, html and css website which calls gemini to extract text from pdfs or images and then breaks that text into chunks which we use chromadb (a RAG framework) to query and get the most related information to a specific question, we then call gemini again to generate questions regarding the topics, and then collect answers from the user. We call gemini one last time to provide feedback on the responses and answer any followup questions the user may have.

### Challenges we ran into

We had issues with data formatting, indexing, flask, html, and debugging. Neither of us were very familiar with html integration with Flask so we had issues trying to figure out how things translated between the languages of html and python. We also had limited experience with the gemini api so understanding how it worked and making it do what we wanted was a little difficult. When using the gemini api, it took a long time to generate questions and answers so debugging it was difficult as we had to wait a long time when trying to figure out what we did wrong.

### Accomplishments we're proud of

Finishing with a fully functional AI powered tool that can help with studying for all people. Becoming much better at HTML and flask. Gaining experience integrating AI into web development

### What we learned

We got more familiar with Web Development, using AI like Gemini, and database management.

### What's next

We are going to add a special user interface for True/False and multiple choice questions. We are also going to improve the AI model of the chatbot used to answer further questions AI answer feedback. We also want to optimize our use of gemini so we get faster responses as our current ones are a little slow.

## README (from the GitHub repository)

# Tailwind 

An AI-powered study assistant built at CruzHacks 2025. Upload your lecture notes
or handwritten notes and Tailwind will automatically generate quizzes to help you
study smarter.

## Features

- Upload typed or handwritten lecture notes
- AI-generated quiz questions based on your content
- Clean web interface for a smooth study experience
- Fast turnaround — upload and start quizzing in seconds

## Tech Stack

- **Frontend:** HTML, CSS
- **Backend:** Python
- **AI:** LLM-powered question generation from uploaded content

## Getting Started

### Prerequisites

- Python 3.8+
- pip


## Detected evidence (automated analysis)

Indexed codebase: 8 recognized source files, 24 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- Python (language) — detected in the code
- Flask (technology) — claimed on Devpost, not found in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (8 of 8)

```
Cruzhacks2025/ai_core.py
Cruzhacks2025/app.py
Cruzhacks2025/config.py
Cruzhacks2025/static/tailwind.css
Cruzhacks2025/templates/index.html
Cruzhacks2025/templates/quiz.html
Cruzhacks2025/templates/results.html
README.md
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Update README.md
- finsihed????
- multiple files
- fixed prompt again
- fixed prompt
- merged ai with website
- Delete Cruzhacks2025/testGemini
- Add files via upload
- Delete Cruzhacks2025 directory
- ai_core parts completed
- merged files together
- new chroma
- Add files via upload
- Initial ai stuff
- Initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### Cruzhacks2025/app.py

```python
from flask import Flask, render_template, request, redirect, url_for, session
from werkzeug.utils import secure_filename
import os
from ai_core import generate_study_questions, evaluate_answers, answer_question
from flask import jsonify
import os
from sentence_transformers import CrossEncoder
from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction
import google.generativeai as genai
from config import genai, chroma_collection, cross_encoder, Base

app = Flask(__name__)
app.secret_key = "supersecretkey"
app.config['UPLOAD_FOLDER'] = 'uploads'

os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == "POST":
        files = request.files.getlist("file")
        file_paths = []

        for file in files:
            if file and file.filename:
                filename = secure_filename(file.filename)
                file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
                file.save(file_path)
                file_paths.append(file_path)

        question_count = int(request.form["question_count"])
        prompt = request.form["prompt"]

        session["file_paths"] = file_paths
        session["question_count"] = question_count
        session["prompt"] = prompt

        print("FILES:", file_paths)
        questions = generate_study_questions(file_paths, prompt, question_count, session_id=90210)
        session["questions"] = questions

        return redirect(url_for("quiz"))

    return render_template("index.html")


@app.route("/quiz", methods=["GET", "POST"])
def quiz():
    questions = session.get("questions", [])
    if request.method == "POST":
        answers = [request.form.get(f"answer_{i}") for i in range(len(questions))]
        session["answers"] = answers
        return redirect(url_for("results"))

    return render_template("quiz.html", questions=questions)


@app.route("/results")
def results():
    questions = session.get("questions", [])
    answers = session.get("answers", [])
    feedback = evaluate_answers(questions, answers,session_id=90210)
    qa_pairs = zip(questions, answers, feedback)
    return render_template("results.html", qa_pairs=qa_pairs)


@app.route("/clarify", methods=["POST"])
def clarify():
    data = request.get_json()
    question = data.get("question", "")
    answer = answer_question(question, session_id=90210)

    return jsonify({"answer": answer})


if __name__ == "__main__":
    app.run(debug=True)
```

### Cruzhacks2025/config.py

```python
import os
import chromadb
from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction
import google.generativeai as genai
from sentence_transformers import CrossEncoder
from sqlalchemy.orm import declarative_base

# --- Gemini API setup ---
genai.configure(api_key=os.environ["GEMINI_API_KEY"])

# --- ChromaDB setup ---
embedding_function = SentenceTransformerEmbeddingFunction()
chroma_client = chromadb.Client()
chroma_collection = chroma_client.get_or_create_collection(
    name="output",
    embedding_function=embedding_function
)

# --- Cross-encoder setup ---
cross_encoder = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

# --- SQLAlchemy base setup ---
Base = declarative_base()

# --- Export for import in other modules ---
__all__ = [
    "genai",
    "chroma_collection",
    "cross_encoder",
    "Base"
]
```

### Cruzhacks2025/ai_core.py

```python
from PIL import Image
from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction
import google.generativeai as genai
import uuid
from sqlalchemy import create_engine, Column, String, Text
from sqlalchemy.orm import sessionmaker
from datetime import datetime, timezone
from pdf2image import convert_from_path
from config import genai, chroma_collection, cross_encoder, Base

class ChatHistory(Base):
    __tablename__ = 'chat_history'
    
    id = Column(String(36), primary_key=True)
    session_id = Column(String(36))
    role = Column(String(10))
    message = Column(Text)
    created_at = Column(String(20), default=lambda: datetime.now(timezone.utc).isoformat())

def init_db():
    engine = create_engine("sqlite:///chat.db", future=True) 
    Base.metadata.create_all(engine)
    Session = sessionmaker(bind=engine, future=True)
    return Session()

def save_message(db_session, session_id, role, message):
    chat_entry = ChatHistory(
        id=str(uuid.uuid4()),
        session_id=session_id,
        role=role,
        message=message
    )
    db_session.add(chat_entry)
    db_session.commit()

def load_chat_history(db_session, session_id):
    history = db_session.query(ChatHistory).filter(
        ChatHistory.session_id == session_id
    ).order_by(ChatHistory.created_at).all()
    
    return [{"role": entry.role, "parts": [entry.message]} for entry in history]

def extract_from_pdf(filepath):
    pages = convert_from_path(filepath)
    words = []
    for page in pages:
        model = genai.GenerativeModel('gemini-2.0-flash')
        response = model.generate_content(["Extract all the text from this image, do not include any other words oustide the text:", page])
        words.append(response.text)
    return "\n".join(words)

def extract_text_from_file(filepath):
    print(filepath + "\n\n\n\n")
    print(str(filepath.rstrip().lower().endswith('.pdf')) + "\n\n\n\n")
    if filepath.rstrip().endswith('.txt'):
        with open(filepath, 'r') as f:
            return f.read()
    elif filepath.rstrip().lower().endswith('.pdf'):
        print("\n\n\nAAAAAAAAAAAAAAAA\n\n\n\n")
        return extract_from_pdf(filepath)
    elif filepath.rstrip().lower().endswith(('.png', '.jpg', '.jpeg')):
        image = Image.open(filepath)
        model = genai.GenerativeModel('gemini-2.0-flash')
        response = model.generate_content(["Extract all the text from this image, do not include any other words oustide the text:", image])
        return response.text
    else:
        raise ValueError("Unsupported file format")

def chunk_text(text, max_len=500):
    paragraphs = text.split("\n")
    chunks, current = [], ""
    for para in paragraphs:
        if len(current) + len(para) < max_len:
            current += para + " "
        else:
            chunks.append(current.strip())
            current = para + " "
    if current:
        chunks.append(current.strip())
    return chunks

def gemini_chat(session_id, question, db_session):
    history = load_chat_history(db_session, session_id)
    model = genai.GenerativeModel('gemini-2.0-flash-lite', system_instruction="You are a helpful expert tutor. Your users are asking questions about information provided by their lecture notes or other sources regarding their class. You will be shown the user's question, and the relevant information, answer the user's question using only this information.")
    chat = model.start_chat(history=history)
    response = model.generate_content(question,generation_config=genai.types.GenerationConfig(
        candidate_count=1,
        temperature=0.3,
    ),)
    save_message(db_session, session_id, "user", question)
    save_message(db_session, session_id, "model", response.text)
    return response.text

def chroma(filepath, chroma_collection):
    text = extract_text_from_file(filepath)
    chunks = chunk_text(text)
    ids = [str(uuid.uuid4()) for _ in chunks]
    chroma_collection.add(ids=ids, documents=chunks)
    return chunks

def retrieve(query, chroma_collection, n_results=10):
    result = chroma_collection.query(query_texts=[query], n_results=n_results, include=["documents"])
    docs = result["documents"][0]
    scores = cross_encoder.predict([[query, doc] for doc in docs])
    reranked = [doc for _, doc in sorted(zip(scores, docs), key=lambda x: x[0], reverse=True)]
    return reranked

def generate_study_questions(filepaths, specification, numqs, session_id):
    query = "Generate study questions based on the following lecture notes. Only include information from the lecture notes, do not include any other information. Make sure to hit on all major points. Make sure to have" + str(numqs) + " questions. The user specfically requested the following: " + str(specification) + ", if it does not make sense ignore it. Do not include any oother characters aside from the question itself, do not include the question numbers. Add a new lince charcter between each question.\n\n"
    db_session = init_db()
    for filepath in filepaths:
        chroma(filepath,chroma_collection)
    retrieved = retrieve(query, chroma_collection)
    combined_info = "\n".join(retrieved)
    response = gemini_chat(session_id, f"{query}\n\nContext:\n{combined_info}", db_session)
    data = response.split('\n\n')
    db_session.close()
    return data

def evaluate_answers(questions, answers, session_id):
    query_prefix = (
        "Correct the following questions and answers. For each one, include:\n"
        "- Feedback on the given answer\n"
        "- The correct answer\n"
        "- A brief explanation of how to get the correct answer\n"
        "- Do not include the question itself simply follow the following format for each question\n"
        "Feedback: \n"
        "Correct Answer: \n"
        "Explanation: \n"
        "Include a newline in between responses to different que
[truncated — 2011 more characters]
```

### Cruzhacks2025/templates/quiz.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Quiz</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link rel="icon" href="/static/tornado.png" />
  <style>
    body {
      font-family: system-ui, sans-serif;
      background-color: #4a4a4a;
      margin: 0;
      padding: 0;
    }

    @keyframes squish {
      0%, 100% {
        transform: scaleX(1);
      }
      50% {
        transform: scaleX(0.6);
      }
    }

    .squish-animation {
      animation: squish 1s ease-in-out infinite;
    }

    @keyframes fadeIn {
      from { opacity: 0; }
      to { opacity: 1; }
    }

    .fade-in {
      animation: fadeIn 0.5s ease-in forwards;
    }
  </style>
</head>
<body class="px-16 pt-8 pb-12">
  <!-- Loading Overlay (starts hidden) -->
  <div id="loading-overlay" class="fixed inset-0 bg-[#4a4a4a] flex flex-col justify-center items-center z-50 hidden">
    <img src="/static/tornado.png" class="w-24 h-24 squish-animation" alt="Loading" />
    <p class="text-white text-2xl mt-4 fade-in">Loading...</p>
  </div>

  <!-- Header -->
  <header class="flex flex-row items-center gap-4 mb-6 justify-center">
    <img src="/static/tornado.png" class="w-20 h-20" alt="Tornado Logo">
    <h1 class="text-4xl font-bold text-white">Tailwind Quiz</h1>
  </header>

  <!-- Quiz Box -->
  <div class="max-w-3xl mx-auto p-8 rounded-3xl" style="background-color: #5a5a5a;">
    <h2 class="text-2xl font-semibold text-white mb-6">Your Quiz</h2>
    <form id="quiz-form" method="POST" class="space-y-6">
      {% for question in questions %}
        <div>
          <p class="text-white font-medium">Question {{ loop.index }}: {{ question }}</p>
          <input type="text" name="answer_{{ loop.index0 }}" class="w-full border border-gray-300 rounded-xl mt-2 p-3 text-white text-lg shadow-sm" style="background-color: #4a4a4a;" required>
        </div>
      {% endfor %}
      <button type="submit" class="bg-purple-600 text-white px-6 py-3 rounded-2xl text-lg hover:bg-purple-500 shadow-lg transition">
        Submit Quiz
      </button>
    </form>
    <a href="/" class="block mt-6 text-purple-300 hover:text-purple-200 underline text-lg">← Go back to upload page</a>
  </div>

  <script>
    // When the form is submitted, show the loading overlay
    document.getElementById('quiz-form').addEventListener('submit', function () {
      document.getElementById('loading-overlay').classList.remove('hidden');
    });
  </script>
</body>
</html>
```

### Cruzhacks2025/templates/results.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Results - Tailwind</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link rel="icon" href="/static/tornado.png" />
  <style>
    body {
      font-family: system-ui, sans-serif;
      background-color: #4a4a4a;
      margin: 0;
      padding-bottom: 100px; /* space for chat input */
    }
  </style>
</head>
<body>

  <div class="px-8 py-12 max-w-4xl mx-auto">
    <header class="flex flex-row items-center gap-4 mb-10 justify-center">
      <img src="/static/tornado.png" class="w-20 h-20" alt="Tornado Logo">
      <h1 class="text-4xl font-bold text-white">Your Results</h1>
    </header>

    <div id="results-container" class="space-y-8">
      {% for q, a, f in qa_pairs %}
        <div class="bg-[#5a5a5a] text-white rounded-2xl p-6 shadow-md border border-gray-500">
          <p class="text-lg font-semibold mb-2">Question {{ loop.index }}:</p>
          <p class="mb-2 text-white/90">{{ q }}</p>
          <p class="text-blue-400 font-medium">Your Answer:</p>
          <p class="mb-2">{{ a }}</p>
          <p class="text-purple-300 italic">AI Feedback:</p>
          <p class="italic">{{ f }}</p>
        </div>
      {% endfor %}
    </div>

    <div id="clarification-log" class="mt-10 space-y-4"></div>

    <a href="/" class="block mt-10 text-purple-400 underline hover:text-purple-300 text-lg">
      ← Go back to upload page
    </a>
  </div>

  <!-- Floating Chat Box -->
  <div class="fixed bottom-0 left-0 right-0 bg-[#5a5a5a] shadow-inner p-4 border-t border-gray-600 z-50">
    <form id="clarifyForm" class="flex gap-3">
      <input
        type="text"
        id="clarifyInput"
        placeholder="Ask a clarifying question..."
        class="flex-1 border border-gray-400 rounded-xl p-3 bg-[#4a4a4a] text-white placeholder-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-500"
        required
      />
      <button class="bg-purple-600 text-white px-6 py-2 rounded-xl hover:bg-purple-500 shadow">
        Send
      </button>
    </form>
  </div>

  <script>
    const form = document.getElementById("clarifyForm");
    const input = document.getElementById("clarifyInput");
    const log = document.getElementById("clarification-log");

    form.addEventListener("submit", async (e) => {
      e.preventDefault();
      const question = input.value.trim();
      if (!question) return;

      // User's message
      const userDiv = document.createElement("div");
      userDiv.className = "bg-[#5a5a5a] rounded-2xl p-4 border border-gray-500 shadow";
      userDiv.innerHTML = `
        <p class="font-semibold text-blue-400">You:</p>
        <p class="ml-2 text-white/90 mt-1">${question}</p>
      `;
      log.appendChild(userDiv);
      input.value = "";

      // Ask backend
      const res = await fetch("/clarify", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ question })
      });

      const data = await res.json();
      const aiResponse = data.answer;

      // AI's response
      const aiDiv = document.createElement("div");
      aiDiv.className = "bg-[#5a5a5a] rounded-2xl p-4 border border-gray-500 shadow";
      aiDiv.innerHTML = `
        <p class="font-semibold text-purple-300">AI:</p>
        <p class="ml-2 italic text-white/80 mt-1">${aiResponse}</p>
      `;
      log.appendChild(aiDiv);

      log.scrollIntoView({ behavior: "smooth", block: "end" });
    });
  </script>
</body>
</html>
```

### Cruzhacks2025/templates/index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Upload Files - Tailwind</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link rel="icon" href="/static/tornado.png" />
  <style>
    body {
      font-family: system-ui, sans-serif;
      background-color: #4a4a4a;
      margin: 0;
      padding: 0;
    }

    input[type="number"]::-webkit-inner-spin-button,
    input[type="number"]::-webkit-outer-spin-button {
      -webkit-appearance: none;
      margin: 0;
    }

    input[type="number"] {
      -moz-appearance: textfield;
    }

    @keyframes squish {
      0%, 100% {
        transform: scaleX(1);
      }
      50% {
        transform: scaleX(0.6);
      }
    }

    .squish-animation {
      animation: squish 1s ease-in-out infinite;
    }

    .fade-in {
      animation: fadeIn 0.5s ease-in forwards;
    }

    @keyframes fadeIn {
      from { opacity: 0; }
      to { opacity: 1; }
    }
  </style>
</head>
<body>
  <!-- Loading Overlay -->
  <div id="loading-overlay" class="fixed inset-0 bg-[#4a4a4a] flex flex-col justify-center items-center z-50 hidden">
    <img src="/static/tornado.png" class="w-24 h-24 squish-animation" alt="Loading" />
    <p class="text-white text-2xl mt-4 fade-in">Loading...</p>
  </div>

  <div class="px-16 py-12">
    <header class="flex flex-row items-center gap-4 mb-6 justify-center">
      <img src="/static/tornado.png" class="w-24 h-24" alt="Tornado Logo">
      <h1 class="text-5xl font-bold text-white">Tailwind</h1>
    </header>

    <form id="uploadForm" action="/" method="POST" enctype="multipart/form-data" class="space-y-10 text-xl">
      <div id="card-container" class="flex flex-wrap gap-5">
        <!-- Add File Card -->
        <div 
          onclick="triggerFileInput()" 
          class="group w-72 h-80 border-2 border-gray-300 border-solid rounded-3xl flex flex-col justify-center items-center cursor-pointer hover:shadow-lg transition"
          style="background-color: #5a5a5a;"
        >
          <div class="text-6xl text-gray-200 group-hover:text-white">+</div>
          <div class="text-base text-gray-200 group-hover:text-white mt-3">Add File</div>
        </div>
      </div>

      <!-- Hidden file input -->
      <input type="file" name="file" class="hidden" id="real-file-input" multiple />

      <!-- Question count -->
      <div class="flex items-center gap-6 text-xl">
        <label for="question_count" class="text-2xl font-semibold text-white whitespace-nowrap">
          How many questions?
        </label>
        <div class="flex items-center gap-4">
          <button
            type="button"
            onclick="adjustCount(-1)"
            class="w-10 h-10 bg-purple-600 text-white rounded-xl text-2xl hover:bg-purple-500 flex items-center justify-center shadow"
          >−</button>
          <input
            type="number"
            id="question_count"
            name="question_count"
            placeholder="e.g. 5"
            class="w-24 border border-gray-300 rounded-2xl p-3 text-center text-xl text-white shadow-sm"
            style="background-color: #5a5a5a;"
            value="5"
            required
          />
          <button
            type="button"
            onclick="adjustCount(1)"
            class="w-10 h-10 bg-purple-600 text-white rounded-xl text-2xl hover:bg-purple-500 flex items-center justify-center shadow"
          >+</button>
        </div>
      </div>

      <!-- Prompt -->
      <textarea
        name="prompt"
        placeholder="Describe what you want the AI to do..."
        class="w-full border border-gray-400 px-6 py-4 rounded-2xl text-lg shadow-md focus:ring-2 focus:ring-purple-500 text-white"
        style="background-color: #5a5a5a;"
        rows="4"
        required
      ></textarea>

      <!-- Submit Button -->
      <div class="flex justify-end relative -top-4">
        <button 
          class="bg-purple-600 text-white px-8 py-4 rounded-2xl text-2xl hover:bg-purple-500 shadow-lg transition" 
          type="submit"
          onclick="showLoading()"
        >
          Start Quiz
        </button>
      </div>
    </form>
  </div>

  <script>
    function triggerFileInput() {
      document.getElementById("real-file-input").click();
    }

    document.getElementById("real-file-input").addEventListener("change", function (e) {
      const container = document.getElementById("card-container");
      const files = e.target.files;

      for (let file of files) {
        const reader = new FileReader();
        reader.onload = function (event) {
          const wrapper = document.createElement("div");
          wrapper.className = "flex flex-col items-center";

          const card = document.createElement("div");
          card.className = "w-72 h-80 rounded-3xl border-2 border-gray-300 bg-[#5a5a5a] overflow-hidden";
          card.innerHTML = `
            <img src="${event.target.result}" class="w-full h-full object-cover" />
          `;

          const filename = document.createElement("div");
          filename.className = "p-2 text-sm text-center text-white truncate w-72 mt-2";
          filename.textContent = file.name;

          wrapper.appendChild(card);
          wrapper.appendChild(filename);
          container.insertBefore(wrapper, container.lastElementChild);
        };
        reader.readAsDataURL(file);
      }
    });

    function adjustCount(delta) {
      const input = document.getElementById('question_count');
      const current = parseInt(input.value) || 0;
      const newValue = Math.max(1, current + delta);
      input.value = newValue;
    }

    function showLoading() {
      const overlay = document.getElementById("loading-overlay");
      overlay.classList.remove("hidden");
    }
  </script>
</body>
</html>
```