# Project export: Bubblify

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: Cal Hacks 10.0
- Tagline: Make emails fun again!
- Devpost: https://devpost.com/software/bubblify
- GitHub: https://github.com/Joshtray/bubblify
- Team: 4 GitHub contributor(s) — Jessey Uche-Nwichi (18 commits), Saad Bin Ihsan (17 commits), polinavishnev (10 commits), Adakole Ebute (1 commits)

## Devpost submission (written by the team)

### Inspiration

We wanted to make regular, mundane tasks better organized than the systems provided because a lot of the times, you feel like you've missed important emails because they came up in a miscategorized within your email. Also, constantly manually filtering each email into a preferred category can get annoying after a while.

### What it does

The app looks at your most recent emails and categorizes them into categories of your choosing -- you can edit the category names whenever!

### How we built it

We connected the Gmail API to CockroachDB and connected to Intel to categorize emails with MindsDB. Then, we coded the front end on Reflex.

### Challenges we ran into

Balancing the expectation for being able to write Pythonic code in Reflex and the shortcomings of what converts it into JavaScript on the front end. And that means we have to modify our logic to be more JavaScript-y in a Pythonic environment. Another issue we ran into was considering the scalability of the application, such as processing multiple requests at the same time.

### Accomplishments we're proud of

Managed to connect so many things together!

### What we learned

How expectations from using previous technologies bias or inform our use of new technologies.

### What's next

We'd love to expand the platform further, streamline it, and increase the capability of handling multiple users.

## README (from the GitHub repository)

# bubblify
A web application to automatically cluster emails into customized bubbles.

# Welcome to Reflex!

This is the base Reflex template - installed when you run `reflex init`.

If you want to use a different template, pass the `--template` flag to `reflex init`.
For example, if you want a more basic starting point, you can run:

```bash
reflex init --template blank
```

## About this Template

This template has the following directory structure:

```bash
├── README.md
├── assets
├── rxconfig.py
└── {your_app}
    ├── __init__.py
    ├── components
    │   ├── __init__.py
    │   └── sidebar.py
    ├── pages
    │   ├── __init__.py
    │   ├── dashboard.py
    │   ├── index.py
    │   └── settings.py
    ├── state.py
    ├── styles.py
    ├── templates
    │   ├── __init__.py
    │   └── template.py
    └── {your_app}.py
```

See the [Project Structure docs](https://reflex.dev/docs/getting-started/project-structure/) for more information on general Reflex project structure.

### Adding Pages

In this template, the pages in your app are defined in `{your_app}/pages/`.
Each page is a function that returns a Reflex component.
For example, to edit this page you can modify `{your_app}/pages/index.py`.
See the [pages docs](https://reflex.dev/docs/components/pages/) for more information on pages.

In this template, instead of using `rx.add_page` or the `@rx.page` decorator,
we use the `@template` decorator from `{your_app}/templates/template.py`.

To add a new page:

1. Add a new file in `{your_app}/pages/`. We recommend using one file per page, but you can also group pages in a single file.
2. Add a new function with the `@template` decorator, which takes the same arguments as `@rx.page`.
3. Import the page in your `{your_app}/pages/__init__.py` file and it will automatically be added to the app.


### Adding Components

In order to keep your code organized, we recommend putting components that are
used across multiple pages in the `{your_app}/components/` directory.

In this template, we have a sidebar component in `{your_app}/components/sidebar.py`.

### Adding State

In this template, we define the base state of the app in `{your_app}/state.py`.
The base state is useful for general app state that is used across multiple pages.

In this template, the base state handles the toggle for the sidebar.

As your app grows, we recommend using [substates](https://reflex.dev/docs/state/substates/)
to organize your state. You can either define substates in their own files, or if the state is
specific to a page, you can define it in the page file itself.


## Detected evidence (automated analysis)

Indexed codebase: 23 recognized source files, 51 KB.
- FastAPI (technology) — detected in the code
- PostgreSQL (technology) — detected in the code
- Python (language) — detected in the code
- Redis (technology) — detected in the code
- LangChain (technology) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (28 of 28)

```
.gitignore
.vscode/settings.json
bubblify/__init__.py
bubblify/bubblify.py
bubblify/components/__init__.py
bubblify/components/bubble.py
bubblify/components/message.py
bubblify/components/sidebar.py
bubblify/helpers/sql_helpers.py
bubblify/pages/__init__.py
bubblify/pages/add_cluster.py
bubblify/pages/index.py
bubblify/pages/login.py
bubblify/pages/settings.py
bubblify/state.py
bubblify/styles.py
bubblify/templates/__init__.py
bubblify/templates/template.py
bubblify/utils/__init__.py
bubblify/utils/auth.py
bubblify/utils/reset.py
bubblify/utils/script.py
certs/ca.crt
my-safe-directory/ca.key
quickstart.py
README.md
requirements.txt
rxconfig.py
```

### Dependencies

- requirements.txt: alembic@==1.12.1, anyio@==4.0.0, async-timeout@==4.0.3, bidict@==0.22.1, cachetools@==5.3.2, certifi@==2023.7.22, charset-normalizer@==3.3.1, click@==8.1.7, cloudpickle@==2.2.1, distro@==1.8.0, exceptiongroup@==1.1.3, fastapi@==0.96.1, google-api-core@==2.12.0, google-api-python-client@==2.105.0, google-auth@==2.23.3, google-auth-httplib2@==0.1.1, google-auth-oauthlib@==1.1.0, googleapis-common-protos@==1.61.0, greenlet@==3.0.1, gunicorn@==20.1.0, h11@==0.14.0, httpcore@==0.17.3, httplib2@==0.22.0, httpx@==0.24.1, idna@==3.4, Jinja2@==3.1.2, Mako@==1.2.4, markdown-it-py@==3.0.0, MarkupSafe@==2.1.3, mdurl@==0.1.2, oauthlib@==3.2.2, packaging@==23.2, pipdeptree@==2.13.0, platformdirs@==3.11.0, protobuf@==4.24.4, psutil@==5.9.6, psycopg@==3.1.12, psycopg-binary@==3.1.12, psycopg2-binary@==2.9.9, pyasn1@==0.5.0, pyasn1-modules@==0.3.0, pydantic@==1.10.13, Pygments@==2.16.1, pyparsing@==3.1.1, python-engineio@==4.8.0, python-multipart@==0.0.5, python-socketio@==5.10.0, redis@==4.6.0, reflex@==0.3.1, requests@==2.31.0, requests-oauthlib@==1.3.1, rich@==13.6.0, rsa@==4.9, simple-websocket@==1.0.0, six@==1.16.0, sniffio@==1.3.0, SQLAlchemy@==1.4.41, sqlalchemy2-stubs@==0.0.2a35, sqlmodel@==0.0.8, starlette@==0.27.0, starlette-admin@==0.9.0, tabulate@==0.9.0, typer@==0.4.2, typing_extensions@==4.8.0, uritemplate@==4.1.1, urllib3@==2.0.7, uvicorn@==0.20.0, watchdog@==2.3.1, watchfiles@==0.19.0, websockets@==10.4, wrapt@==1.15.0, wsproto@==1.2.0

### Recent commits (newest first)

- Merge pull request #10 from Joshtray/list-messages
- message.py
- Refresh ability
- Merge branch 'main' of https://github.com/Joshtray/bubblify into list-messages
- Merge branch 'main' of https://github.com/Joshtray/bubblify
- Fixed merge conflicts
- Used langchain to automatically sort shit
- Added unread messages
- Reflexive states based on loggin in
- Merge pull request #9 from Joshtray/frontend
- Fixed bubbles
- removing cluster
- local commits
- Merge pull request #7 from Joshtray/frontend
- req
- added requirements
- Removed cache
- Fixed merge conflict
- Added focus mode
- Merge pull request #6 from Joshtray/new-branch

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

### requirements.txt

```
alembic==1.12.1
anyio==4.0.0
async-timeout==4.0.3
bidict==0.22.1
cachetools==5.3.2
certifi==2023.7.22
charset-normalizer==3.3.1
click==8.1.7
cloudpickle==2.2.1
distro==1.8.0
exceptiongroup==1.1.3
fastapi==0.96.1
google-api-core==2.12.0
google-api-python-client==2.105.0
google-auth==2.23.3
google-auth-httplib2==0.1.1
google-auth-oauthlib==1.1.0
googleapis-common-protos==1.61.0
greenlet==3.0.1
gunicorn==20.1.0
h11==0.14.0
httpcore==0.17.3
httplib2==0.22.0
httpx==0.24.1
idna==3.4
Jinja2==3.1.2
Mako==1.2.4
markdown-it-py==3.0.0
MarkupSafe==2.1.3
mdurl==0.1.2
oauthlib==3.2.2
packaging==23.2
pipdeptree==2.13.0
platformdirs==3.11.0
protobuf==4.24.4
psutil==5.9.6
psycopg==3.1.12
psycopg-binary==3.1.12
psycopg2-binary==2.9.9
pyasn1==0.5.0
pyasn1-modules==0.3.0
pydantic==1.10.13
Pygments==2.16.1
pyparsing==3.1.1
python-engineio==4.8.0
python-multipart==0.0.5
python-socketio==5.10.0
redis==4.6.0
reflex==0.3.1
requests==2.31.0
requests-oauthlib==1.3.1
rich==13.6.0
rsa==4.9
simple-websocket==1.0.0
six==1.16.0
sniffio==1.3.0
SQLAlchemy==1.4.41
sqlalchemy2-stubs==0.0.2a35
sqlmodel==0.0.8
starlette==0.27.0
starlette-admin==0.9.0
tabulate==0.9.0
typer==0.4.2
typing_extensions==4.8.0
uritemplate==4.1.1
urllib3==2.0.7
uvicorn==0.20.0
watchdog==2.3.1
watchfiles==0.19.0
websockets==10.4
wrapt==1.15.0
wsproto==1.2.0

```

### bubblify/pages/index.py

```python
"""The home page of the app."""

from bubblify import styles
from bubblify.templates import template
from bubblify.state import State
from bubblify.components.bubble import bubble

import reflex as rx


@template(route="/", title="Home", image="/home.svg", on_load=State.get_clusters)
def index() -> rx.Component:
    """The home page.

    Returns:
        The UI for the home page.
    """

    return rx.cond(
        State.authenticated_user,
        rx.vstack(
            rx.foreach(State.clusters, bubble),
            width="100%",
            height="100%",
            position="relative",
        ),
        rx.vstack(
            rx.heading("Please login to view your clusters"),
            rx.link("Login", href="/login"),
            width="100%",
            height="100%",
            justify_content="center",
            align_items="center",
        ),
    )

```

### rxconfig.py

```python
import reflex as rx

config = rx.Config(
    app_name="bubblify",
)
```

### quickstart.py

```python
from __future__ import print_function

import os.path

from bubblify.helpers.sql_helpers import (
    get_json_from_database,
    create_categories,
    insert_email_info,
    execute_sql_query,
    conn,
    insert_categorized_email,
    create_emails_info_table,
)
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
import datetime
import json
import logging
import os
import random
import time
import uuid
from argparse import ArgumentParser, RawTextHelpFormatter


SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]


def main():
    """Shows basic usage of the Gmail API.
    Lists the user's Gmail labels.
    """
    creds = None
    # The file token.json stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists("token.json"):
        creds = Credentials.from_authorized_user_file("token.json", SCOPES)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
            flow.redirect_uri = "http://127.0.0.1:8080/"  # Use your predefined URI here
            creds = flow.run_local_server(port=8080)
        # Save the credentials for the next run
        with open("token.json", "w") as token:
            token.write(creds.to_json())

    try:
        # Call the Gmail API
        service = build("gmail", "v1", credentials=creds)
        results = service.users().messages().list(userId="me", maxResults=20).execute()
        messages = results.get("messages", [])

        if not messages:
            print("No messages found.")
            return
        print("Messages:")

        email_info_data = []

        for msg in messages:
            message = (
                service.users().messages().get(userId="me", id=msg["id"]).execute()
            )

            # Extract the date
            date = message["internalDate"]
            date = datetime.datetime.fromtimestamp(int(date) / 1000).strftime(
                "%Y-%m-%d %H:%M:%S"
            )

            # Check if the email is unread
            is_unread = "UNREAD" in message["labelIds"]

            subject = None
            for header in message["payload"]["headers"]:
                if header["name"] == "Subject":
                    subject = header["value"]
                    break

            # Extract the sender
            sender = None
            for header in message["payload"]["headers"]:
                if header["name"] == "From":
                    sender = header["value"]
                    break

            # Extract the snippet
            snpt = message["snippet"]  # TODO: make this a config var

            # Extract all label tags
            label_tags = [label for label in message["labelIds"] if label != "UNREAD"]

            email_info_data.append((snpt, is_unread, subject, sender, date))

        # Insert data into the emails_info table using execute_values
        email_info_ids = insert_email_info(conn, email_info_data)

        # Create categories for labels
        category_ids = create_categories(conn, label_tags)

        for idx, email_info_id in enumerate(email_info_ids):
            # Insert data into the categorized_emails table
            # For simplicity, let's assume you associate each email with the first category
            if idx < len(category_ids):
                category_id = category_ids[idx]
            else:
                # If there are more emails than categories, use the last category (or any default behavior)
                category_id = category_ids[-1]

            # Insert data into the categorized_emails table
            insert_categorized_email(conn, email_info_id, category_id)

    except HttpError as error:
        # TODO(developer) - Handle errors from gmail API.
        print(f"An error occurred: {error}")


if __name__ == "__main__":
    create_emails_info_table()
    main()
    get_json_from_database()

```

### bubblify/__init__.py

```python
"""Base template for Reflex."""

```

### bubblify/bubblify.py

```python
"""Welcome to Reflex!."""

from bubblify import styles
from dotenv import load_dotenv

# Import all the pages.
from bubblify.pages import *
import dotenv 
dotenv.load_dotenv()
import reflex as rx

load_dotenv()

# Create the app and compile it.
app = rx.App(styles=styles.base_style)
app.compile()

```

### bubblify/styles.py

```python
"""Styles for the app."""

import reflex as rx

border_radius = "0.375rem"
box_shadow = "0px 0px 0px 1px rgba(84, 82, 95, 0.14)"
border = "3px solid black"
text_color = "black"
accent_text_color = "#1A1060"
accent_color = "#F5EFFE"
hover_accent_color = {"_hover": {"color": accent_color}}
hover_accent_bg = {"_hover": {"bg": accent_color}}
content_width_vw = "90vw"
sidebar_width = "20em"
min_bubble_size = 30
max_bubble_size = 700

template_page_style = {"width": "100vw", "height": "100vh"}

template_content_style = {
    "width": "100%",
    "height": "100%",
    "border_radius": border_radius,
}

link_style = {
    "color": text_color,
    "text_decoration": "none",
    **hover_accent_color,
}

overlapping_button_style = {
    "background_color": "white",
    "border": border,
    "border_radius": border_radius,
}

base_style = {
    rx.MenuButton: {
        "width": "3em",
        "height": "3em",
        **overlapping_button_style,
    },
    rx.MenuItem: hover_accent_bg,
}

markdown_style = {
    "code": lambda text: rx.code(text, color="#1F1944", bg="#EAE4FD"),
    "a": lambda text, **props: rx.link(
        text,
        **props,
        font_weight="bold",
        color="#03030B",
        text_decoration="underline",
        text_decoration_color="#AD9BF8",
        _hover={
            "color": "#AD9BF8",
            "text_decoration": "underline",
            "text_decoration_color": "#03030B",
        },
    ),
}

```

### bubblify/state.py

```python
"""Base state for the app."""

from langchain.chat_models import ChatOpenAI

import os

import reflex as rx

from collections import Counter

import random

from bubblify import styles

from .utils.auth import Auth

from bubblify.helpers.sql_helpers import (
    get_json_from_database,
    create_categories,
    insert_email_info,
    execute_sql_query,
    conn,
    insert_categorized_email,
    create_emails_info_table,
)
from quickstart import main

llm = ChatOpenAI(openai_api_key=os.getenv("OPENAI_API_KEY"))


class State(rx.State):
    """Base state for the app.

    The base state is used to store general vars used throughout the app.
    """

    email_data: list[dict] = [
        {
            "sender": "GitGuardian <security@getgitguardian.com>",
            "snippet": "GitGuardian has detected the following Google OAuth2 Keys exposed within your GitHub account. Details - Secret type: Google OAuth2 Keys - Repository: Joshtray/bubblify - Pushed date: October 29th 2023,",
            "subject": "[Joshtray/bubblify] Google OAuth2 Keys exposed on GitHub",
            "date_received": "2023-10-28",
            "unread": False,
            "category_name": "INBOX",
        },
        {
            "sender": "John Doe <john.doe@example.com>",
            "snippet": "Hello, just checking in on our project progress. How's it going?",
            "subject": "Project Progress",
            "date_received": "2023-10-27",
            "unread": True,
            "category_name": "INBOX",
        },
        {
            "sender": "Alice Smith <alice.smith@example.com>",
            "snippet": "Meeting reminder for next week. Don't forget to prepare the presentation.",
            "subject": "Meeting Reminder",
            "date_received": "2023-10-26",
            "unread": False,
            "category_name": "INBOX",
        },
        {
            "sender": "Support Team <support@example.com>",
            "snippet": "Your support ticket #12345 has been resolved. If you have any more questions, feel free to ask.",
            "subject": "Support Ticket Resolution",
            "date_received": "2023-10-25",
            "unread": False,
            "category_name": "INBOX",
        },
        {
            "sender": "Jane Williams <jane.williams@example.com>",
            "snippet": "Weekly report attached. Please review and provide feedback.",
            "subject": "Weekly Report",
            "date_received": "2023-10-24",
            "unread": True,
            "category_name": "INBOX",
        },
        {
            "sender": "Free Offers <offers@example.com>",
            "snippet": "Congratulations! You've won a free cruise vacation. Click here to claim your prize!",
            "subject": "Free Cruise Offer",
            "date_received": "2023-10-27",
            "unread": False,
            "category_name": "Spam",
        },
        {
            "sender": "Growth Hacks <hacks@example.com>",
            "snippet": "Get rich quick with our amazing investment opportunity. Don't miss out!",
            "subject": "Investment Opportunity",
            "date_received": "2023-10-26",
            "unread": False,
            "category_name": "Spam",
        },
        {
            "sender": "Unsolicited Newsletter <newsletter@example.com>",
            "snippet": "You're receiving this email because you subscribed to our newsletter. To unsubscribe, click here.",
            "subject": "Weekly Newsletter",
            "date_received": "2023-10-25",
            "unread": False,
            "category_name": "Spam",
        },
        {
            "sender": "Amazon Deals <deals@amazon.com>",
            "snippet": "Check out our latest deals and discounts on electronics, clothing, and more!",
            "subject": "Amazon Promotions",
            "date_received": "2023-10-28",
            "unread": False,
            "category_name": "Promotions",
        },
        {
            "sender": "Tech Store <info@techstore.com>",
            "snippet": "Exclusive offer for tech enthusiasts: 20% off on all gadgets this week only!",
            "subject": "Tech Store Promotion",
            "date_received": "2023-10-27",
            "unread": False,
            "category_name": "Promotions",
        },
        {
            "sender": "Fashion Outlet <sales@fashionoutlet.com>",
            "snippet": "New arrivals and special discounts on fashion items. Shop now!",
            "subject": "Fashion Outlet Sale",
            "date_received": "2023-10-26",
            "unread": False,
            "category_name": "Promotions",
        },
        {
            "sender": "Travel Discounts <info@traveldiscounts.com>",
            "snippet": "Plan your next vacation with our exclusive travel deals and discounts!",
            "subject": "Travel Discounts",
            "date_received": "2023-10-25",
            "unread": False,
            "category_name": "Promotions",
        },
        {
            "sender": "Food Delivery <offers@fooddelivery.com>",
            "snippet": "Get 10% off on your next food delivery order. Use code: DELICIOUS10",
            "subject": "Food Delivery Discount",
            "date_received": "2023-10-24",
            "unread": False,
            "category_name": "Promotions",
        },
    ]

    clusters: list[tuple[str, int, list, float, float, float]] = []
    index_index: int = 0
    name_index: int = 1
    size_index: int = 2
    messages_index: int = 3
    diameter_index: int = 4
    positionx_index: int = 5
    positiony_index: int = 6
    color_index: int = 7
    z_index_index: int = 8
    unread_count_index: int = 9

    colors: list[str] = [
        "#d27cbf",
        "#d2bf7c",
        "#7cb3d2",
        "#7cd2be",
        "#d27c7c",
        "#7cd2b3",
        "#d27cbf",
        "#7cbfd2",
    ]
    cluster_names: list[str] = ["Work", "Social", "Urgent", "Other"]
    new_cluster_name: str = ""
    current_email: str = ""
    current_password: str = ""
    authe
[truncated — 13364 more characters]
```

### bubblify/templates/__init__.py

```python
from .template import template

```

### bubblify/pages/__init__.py

```python
from .index import index
from .settings import settings
from .add_cluster import add_cluster
from .login import login
```

[11 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]