# Project export: AI Boyfriend

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: AI Boyfriend is your go-to digital companion for meal decisions. In just a few taps, it learns your tastes and dietary needs to recommend the perfect meal. Say goodbye to mealtime indecision!
- Devpost: https://devpost.com/software/ai-boyfriend
- GitHub: https://github.com/taeyangcode/ai-boyfriend
- Team: 4 GitHub contributor(s) — jengoc415 (54 commits), bryanwonggy (40 commits), taeyangcode (25 commits), Yuta Yamada (3 commits)

## Devpost submission (written by the team)

### Inspiration

The inspiration for AI Boyfriend came from the common dilemma of not knowing what to eat. We wanted to create a tool that not only suggests meals but also understands and adapts to individual preferences, dietary needs, and moods, making meal selection a stress-free experience.

### What it does

AI Boyfriend is an app that serves as a digital culinary guide. It uses smart algorithms and a series of intuitive questions generated by LLMs to understand a user's preferences, then curates personalized meal suggestions. It simplifies the decision-making process for meals by providing targeted, tailored recommendations.

### How we built it

We developed AI Boyfriend by using Large Language Models (LLMs) to generate interactive, conversational questionnaires. The app integrates with Yelp's API to offer a wide range of meal suggestions. The user-friendly interface was designed with a focus on simplicity and ease of use.

### Challenges we ran into

One of the biggest challenges was refining the AI to accurately interpret and respond to a diverse set of dietary preferences and restrictions. Ensuring the app consistently provided relevant and varied suggestions without overwhelming the user was also a significant hurdle.

### Accomplishments we're proud of

As newcomers to hacking, we're proud of successfully developing an app that integrates LLMs for meal decision-making. Despite our limited experience, we overcame technical challenges, learning and applying different APIs rapidly. Our teamwork stands out – different skills and backgrounds merged effectively, crucial for the app’s success and positive user feedback, especially on its intuitive interface. This achievement in a high-pressure, hackathon environment not only demonstrates our technical growth but also our strong collaborative dynamic.

### What we learned

Through this project, we learned a great deal about user experience design, advanced machine learning techniques, and the intricacies of dietary preferences. We also gained insights into managing and analyzing large data sets to provide meaningful recommendations.

### What's next

AI Boyfriend is set to become a full-fledged lifestyle assistant, expanding its scope to include event and activity recommendations: Event Integration: It will suggest personalized events like concerts and exhibitions, based on user interests. Activity Suggestions: The app will recommend activities tailored to users’ preferences and current conditions like weather. Social Planning: Users can plan and coordinate group events, considering everyone's preferences and schedules. Customized Itineraries: AI Boyfriend will create tailored itineraries combining dining and activities for full-day experiences. Feedback-Driven Learning: User reviews will refine the AI’s suggestions, ensuring relevancy and satisfaction. Local Partnerships: Collaborating with businesses for exclusive deals and experiences, enhancing the entertainment options. Adaptive Recommendations: The app will evolve with users, providing ideas for family, romantic, or solo adventures based on life stages. This upgrade will transform AI Boyfriend into a comprehensive guide for dining, events, and activities, adapting to the dynamic needs of modern lifestyles.

## README (from the GitHub repository)

# AI Boyfriend

CalHacks 10.0 submission.

## Setup

```bash
# Instructions for starting the server locally

# Install
npm install && npm run build

# Start server
npm run dev

# Start server and watch for changes
npm run dev:watch
```

### Environment File

```env
YELP_API_KEY="..."
OPENAI_API_KEY="..."
GOOGLE_MAPS_API_KEY="..."
```

## Devpost

https://devpost.com/software/ai-boyfriend

### Inspiration

The inspiration for AI Boyfriend came from the common dilemma of not knowing what to eat. We wanted to create a tool that not only suggests meals but also understands and adapts to individual preferences, dietary needs, and moods, making meal selection a stress-free experience.

### What it does

AI Boyfriend is an app that serves as a digital culinary guide. It uses smart algorithms and a series of intuitive questions generated by LLMs to understand a user's preferences, then curates personalized meal suggestions. It simplifies the decision-making process for meals by providing targeted, tailored recommendations.

### How we built it

We developed AI Boyfriend by using Large Language Models (LLMs) to generate interactive, conversational questionnaires. The app integrates with Yelp's API to offer a wide range of meal suggestions. The user-friendly interface was designed with a focus on simplicity and ease of use.

### Challenges we ran into

One of the biggest challenges was refining the AI to accurately interpret and respond to a diverse set of dietary preferences and restrictions. Ensuring the app consistently provided relevant and varied suggestions without overwhelming the user was also a significant hurdle.

### Accomplishments that we're proud of

As newcomers to hacking, we're proud of successfully developing an app that integrates LLMs for meal decision-making. Despite our limited experience, we overcame technical challenges, learning and applying different APIs rapidly.

Our teamwork stands out – different skills and backgrounds merged effectively, crucial for the app’s success and positive user feedback, especially on its intuitive interface. This achievement in a high-pressure, hackathon environment not only demonstrates our technical growth but also our strong collaborative dynamic.

### What we learned

Through this project, we learned a great deal about user experience design, advanced machine learning techniques, and the intricacies of dietary preferences. We also gained insights into managing and analyzing large data sets to provide meaningful recommendations.

### What's next for AI Boyfriend

AI Boyfriend is set to become a full-fledged lifestyle assistant, expanding its scope to include event and activity recommendations:

-   Event Integration: It will suggest personalized events like concerts and exhibitions, based on user interests.
-   Activity Suggestions: The app will recommend activities tailored to users’ preferences and current conditions like weather.
-   Social Planning: Users can plan and coordinate group events, considering everyone's preferences and schedules.
-   Customized Itineraries: AI Boyfriend will create tailored itineraries combining dining and activities for full-day experiences.
-   Feedback-Driven Learning: User reviews will refine the AI’s suggestions, ensuring relevancy and satisfaction.
-   Local Partnerships: Collaborating with businesses for exclusive deals and experiences, enhancing the entertainment options.
-   Adaptive Recommendations: The app will evolve with users, providing ideas for family, romantic, or solo adventures based on life stages.

This upgrade will transform AI Boyfriend into a comprehensive guide for dining, events, and activities, adapting to the dynamic needs of modern lifestyles.


## Detected evidence (automated analysis)

Indexed codebase: 19 recognized source files, 49 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (26 of 26)

```
.eslintrc.cjs
.gitignore
.prettierrc
api/main.py
api/requirements.txt
index.html
package.json
postcss.config.js
README.md
src/App.tsx
src/Helper/Helper.tsx
src/index.css
src/LocationCard/LocationCard.tsx
src/main.tsx
src/Notification/Notification.tsx
src/Questionnaire/Choice.tsx
src/Questionnaire/Question.tsx
src/Questionnaire/Questionnaire.tsx
src/Result/Result.tsx
src/types.ts
src/UserFilter/UserFilter.tsx
src/vite-env.d.ts
tailwind.config.js
tsconfig.json
tsconfig.node.json
vite.config.ts
```

### Dependencies

- api/requirements.txt: aiohttp@==3.8.6, aiosignal@==1.3.1, annotated-types@==0.6.0, anyio@==3.7.1, async-timeout@==4.0.3, attrs@==23.1.0, certifi@==2023.7.22, charset-normalizer@==3.3.1, click@==8.1.7, exceptiongroup@==1.1.3, fastapi@==0.104.0, frozenlist@==1.4.0, h11@==0.14.0, httptools@==0.6.1, idna@==3.4, multidict@==6.0.4, openai@==0.28.1, pydantic@==2.4.2, pydantic_core@==2.10.1, python-dotenv@==1.0.0, pytz@==2023.3.post1, PyYAML@==6.0.1, requests@==2.31.0, sniffio@==1.3.0, sseclient-py@==1.8.0, starlette@==0.27.0, tabulate@==0.9.0, tqdm@==4.66.1, typer@==0.9.0, typing_extensions@==4.8.0, urllib3@==2.0.7, uvicorn@==0.23.2, uvloop@==0.19.0, watchfiles@==0.21.0, websockets@==12.0, yarl@==1.9.2
- package.json: @types/react@^18.2.15, @types/react-datepicker@^4.19.1, @types/react-dom@^18.2.7, @typescript-eslint/eslint-plugin@^6.0.0, @typescript-eslint/parser@^6.0.0, @vitejs/plugin-react@^4.0.3, autoprefixer@^10.4.16, chokidar-cli@^3.0.0, concurrently@^8.2.2, eslint@^8.45.0, eslint-plugin-react-hooks@^4.6.0, eslint-plugin-react-refresh@^0.4.3, postcss@^8.4.31, prettier@^3.0.3, prettier-plugin-tailwindcss@^0.5.6, react@^18.2.0, react-datepicker@^4.21.0, react-dom@^18.2.0, tailwindcss@^3.3.5, typescript@^5.0.2, vite@^4.4.5

### Recent commits (newest first)

- readme update
- readme update
- v1 published :)
- final
- result page update
- fixed merge conflict error
- open new tab
- result page update
- update prompt
- result page update
- UI fixes for questionnaire
- Merge branch 'dev' of https://github.com/taeyangcode/ai-boyfriend into dev
- UI changes for questionnaire
- result page complete
- Merge branch 'jeng/ui' of https://github.com/taeyangcode/ai-boyfriend into dev
- Merge branch 'dev' of https://github.com/taeyangcode/ai-boyfriend into jeng/ui
- Merge branch 'dev' of https://github.com/taeyangcode/ai-boyfriend into dev
- WE FIXED IT FOR REAL PT3
- UI changes for UserFilter
- UI changes for UserFilter

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

### package.json

```
{
    "name": "ai-boyfriend",
    "private": true,
    "version": "0.0.0",
    "type": "module",
    "scripts": {
        "dev": "cd api && source ./venv/bin/activate && uvicorn main:app --reload",
        "watch": "chokidar '**/*.tsx' '**/*.css' --ignore '**/node_modules/**' --ignore '**/dist/**' -c 'npm run build'",
        "dev:watch": "concurrently \"npm run dev\" \"npm run watch\"",
        "build": "tsc && vite build",
        "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
        "preview": "vite preview"
    },
    "dependencies": {
        "autoprefixer": "^10.4.16",
        "chokidar-cli": "^3.0.0",
        "concurrently": "^8.2.2",
        "postcss": "^8.4.31",
        "react": "^18.2.0",
        "react-datepicker": "^4.21.0",
        "react-dom": "^18.2.0"
    },
    "devDependencies": {
        "@types/react": "^18.2.15",
        "@types/react-datepicker": "^4.19.1",
        "@types/react-dom": "^18.2.7",
        "@typescript-eslint/eslint-plugin": "^6.0.0",
        "@typescript-eslint/parser": "^6.0.0",
        "@vitejs/plugin-react": "^4.0.3",
        "eslint": "^8.45.0",
        "eslint-plugin-react-hooks": "^4.6.0",
        "eslint-plugin-react-refresh": "^0.4.3",
        "prettier": "^3.0.3",
        "prettier-plugin-tailwindcss": "^0.5.6",
        "tailwindcss": "^3.3.5",
        "typescript": "^5.0.2",
        "vite": "^4.4.5"
    }
}

```

### api/requirements.txt

```
aiohttp==3.8.6
aiosignal==1.3.1
annotated-types==0.6.0
anyio==3.7.1
async-timeout==4.0.3
attrs==23.1.0
certifi==2023.7.22
charset-normalizer==3.3.1
click==8.1.7
exceptiongroup==1.1.3
fastapi==0.104.0
frozenlist==1.4.0
h11==0.14.0
httptools==0.6.1
idna==3.4
multidict==6.0.4
openai==0.28.1
pydantic==2.4.2
pydantic_core==2.10.1
python-dotenv==1.0.0
pytz==2023.3.post1
PyYAML==6.0.1
requests==2.31.0
sniffio==1.3.0
sseclient-py==1.8.0
starlette==0.27.0
tabulate==0.9.0
tqdm==4.66.1
typer==0.9.0
typing_extensions==4.8.0
urllib3==2.0.7
uvicorn==0.23.2
uvloop==0.19.0
watchfiles==0.21.0
websockets==12.0
yarl==1.9.2

```

### src/main.tsx

```typescript
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";

ReactDOM.createRoot(document.getElementById("root")!).render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

```

### src/App.tsx

```typescript
import { useState } from 'react'
import UserFilter from './UserFilter/UserFilter'
import Questionnaire from './Questionnaire/Questionnaire'
import Result from './Result/Result'
import NotificationHandler from './Notification/Notification'
import '../src/index.css'

function App() {
    const [notifications, setNotifications] = useState<Array<NotificationType>>([])
    const [selectedPage, setSelectedPage] = useState<Page>('preference')
    const [responseChain, setResponseChain] = useState<ResponseChain>()
    console.log('responseChain: ', responseChain)

    // Questions and choices for questionnaire page
    const [question, setQuestion] = useState<string>('Hmmm let me think...')
    const [choices, setChoices] = useState<Array<string>>([])

    // ID for result page
    const [restaurantId, setRestaurantId] = useState<Array<string>>([])

    // function to change pages; passed into all components as props
    const changePage = (newPage: Page, newResponseChain?: ResponseChain) => {
        console.info('new response chain app', newResponseChain)
        setSelectedPage(newPage)
        setResponseChain(newResponseChain)
        console.info('response chain app', responseChain)
        console.log('RENDERING NEW PAGE')
        console.log('new page: ', selectedPage)
        console.log('new response chain: ', responseChain)
        renderPage()
    }

    const renderPage = () => {
        switch (selectedPage) {
            case 'preference':
                return (
                    <UserFilter
                        notifications={notifications}
                        setNotifications={setNotifications}
                        changePage={changePage}
                        setQuestion={setQuestion}
                        setChoices={setChoices}
                        setRestaurantId={setRestaurantId}
                        setResponseChain={setResponseChain}
                    />
                )
            case 'questionnaire':
                return (
                    <Questionnaire
                        changePage={changePage}
                        setRestaurantId={setRestaurantId}
                        question={question}
                        setQuestion={setQuestion}
                        choices={choices}
                        setChoices={setChoices}
                        responseChain={responseChain}
                        setResponseChain={setResponseChain}
                    />
                )
            case 'result':
                return <Result locationIds={restaurantId} />
            default:
                return (
                    <>
                        <h1>404 Page not found</h1>
                    </>
                )
        }
    }

    return (
        <div className="h-screen bg-gradient-to-br from-blue-900 via-blue-200 to-blue-500 bg-cover bg-center bg-no-repeat p-12">
            <NotificationHandler notifications={notifications} />
            {renderPage()}
        </div>
    )
}

export default App

```

### api/main.py

```python
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
import requests
import datetime
import pytz
from dotenv import load_dotenv
import os
import openai
from pydantic import BaseModel, Field
from typing import List, Optional
from starlette.responses import FileResponse

## Load environment variables
load_dotenv()

openai.api_key = os.getenv("OPENAI_API_KEY")
yelp_api_key = os.getenv("YELP_API_KEY")
googleGeoLoc_api_key = os.getenv("GOOGLE_MAPS_API_KEY")

yelp_base_url = "https://api.yelp.com/v3/"
tz = pytz.timezone("America/Los_Angeles")


## Define models
class UserInput(BaseModel):
    longitude: float
    latitude: float
    price: int
    radius: int
    date: int
    dietary_preferences: List[str]


class Message(BaseModel):
    role: str
    content: Optional[str] = None
    function_call: Optional[dict] = None

    def to_dict(self):
        if self.role == "user":
            return self.dict(exclude={"function_call"})
        return self.dict()


class Messages(BaseModel):
    messages: list[Message]


class AIInput(BaseModel):
    input: UserInput
    messages: Optional[Messages] = None


class GeoInput(BaseModel):
    address: str
    city: str
    state: str


## Define apps
app = FastAPI(title="main app")
api_app = FastAPI(title="api app")
origins = ["http://localhost:8000"]
api_app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
app.mount("/api", api_app)
app.mount("/", StaticFiles(directory="../dist", html=True), name="ui")


## MAIN APP
@app.get("/")
async def read_index():
    return FileResponse("../dist/index.html")


## API APP
@api_app.get("/")
async def test():
    return {"message": "Hello World"}


@api_app.post("/businesses")
async def get_businesses(input: UserInput):
    search_url = f"{yelp_base_url}businesses/search"
    headers = {
        "accept": "application/json",
        "Authorization": f"Bearer {yelp_api_key}",
    }
    payload = {
        "term": "food",
        "limit": 20,  # max of 50 results
        "longitude": input.longitude,
        "latitude": input.latitude,
        "price": enumerate_to(input.price),
        "radius": input.radius,
        "open_at": input.date,
    }
    try:
        response = requests.get(search_url, headers=headers, params=payload)
        if response.ok:
            return response.json()
        else:
            return {"error": response.status_code}
    except Exception as e:
        print("Error fetching businesses: ", str(e))
        return None


@api_app.post("/businesses/{id}")
async def get_business(id: str):
    search_url = f"{yelp_base_url}businesses/{id}"
    headers = {
        "accept": "application/json",
        "Authorization": f"Bearer {yelp_api_key}",
    }
    payload = {"business_id_or_alias": "id"}
    try:
        response = requests.get(search_url, headers=headers, params=payload)
        if response.ok:
            return response.json()
        else:
            return {"error": response.status_code}
    except Exception as e:
        print("Error fetching business: ", str(e))
        return None


## GOOGLE GEOLOCATION API
@api_app.post("/geolocation")
async def get_coordinates_forAddress(input: GeoInput):
    api_url = "https://maps.googleapis.com/maps/api/geocode/json"

    params = {
        "address": input.address + ", " + input.city + ", " + input.state,
        "key": googleGeoLoc_api_key,
    }

    try:
        response = requests.get(api_url, params=params)
        data = response.json()

        if response.ok:
            result = data["results"][0]
            location = result["geometry"]["location"]
            lat = location["lat"]
            lng = location["lng"]
            return {"lat": lat, "lng": lng}
        else:
            return None

    except Exception as e:
        print("Error fetching coordinates: ", str(e))
        return None


## OPENAI API
@api_app.post("/get_question")
async def get_question(messages: Messages):
    functions = [
        {
            "name": "get_question",
            "description": "Get question to narrow down list of restaurants",
            "parameters": {
                "type": "object",
                "properties": {
                    "have_result": {
                        "type": "boolean",
                        "description": "Whether the model has a final result from the list of restaurants",
                    },
                    "question": {
                        "type": "string",
                        "description": "The question to ask the user",
                    },
                    "choices": {
                        "type": "array",
                        "description": "The choices for answers to the corresponding question",
                        "items": {
                            "type": "string",
                            "properties": {
                                "choice": {
                                    "type": "string",
                                }
                            },
                        },
                    },
                },
                "required": ["question", "choices", "have_result"],
            },
        },
    ]

    ## Format messages to dict from Messages object
    messages_formatted = [message.to_dict() for message in messages.messages]
    try:
        response = openai.ChatCompletion.create(
            model="gpt-4",
            messages=messages_formatted,
            functions=functions,
            function_call={"name": "get_question"},
        )
        response_message = response["choices"][0]["message"]
        messages_formatted.append(response_message)

        return {
            "latest_response": response_message,
            "messages": messages_formatted,
        }
    except Exception as e:
        print("Error fetching question: ", str(e))
      
[truncated — 5700 more characters]
```

### postcss.config.js

```javascript
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

```

### vite.config.ts

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
})

```

### tailwind.config.js

```javascript
/** @type {import('tailwindcss').Config} */
export default {
    content: ["./src/**/*.{js,jsx,ts,tsx}"],
    theme: {
        extend: {},
    },
    plugins: [],
};

```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

### src/vite-env.d.ts

```typescript
/// <reference types="vite/client" />

```

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