Initial Code Commit

This commit is contained in:
2025-10-24 02:07:59 -04:00
commit f099f36838
63 changed files with 4425 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
images/
File Converter/pnpm-lock.yaml

58
AI Training/ai.ipynb Executable file
View File

@@ -0,0 +1,58 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"import dask.dataframe as dd"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" image label\n",
"0 {'bytes': b'\\xff\\xd8\\xff\\xe0\\x00\\x10JFIF\\x00\\x... 0\n",
"1 {'bytes': b'\\xff\\xd8\\xff\\xe0\\x00\\x10JFIF\\x00\\x... 0\n",
"2 {'bytes': b'\\xff\\xd8\\xff\\xe0\\x00\\x10JFIF\\x00\\x... 0\n",
"3 {'bytes': b'\\xff\\xd8\\xff\\xe0\\x00\\x10JFIF\\x00\\x... 0\n",
"4 {'bytes': b'\\xff\\xd8\\xff\\xe0\\x00\\x10JFIF\\x00\\x... 0\n"
]
}
],
"source": [
"df = dd.read_parquet(\"hf://datasets/edwinpalegre/trashnet_enhanced/data/train-*.parquet\")\n",
"\n",
"print(df.show())"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.5"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

Binary file not shown.

6
AI Training/hf.py Executable file
View File

@@ -0,0 +1,6 @@
# Load model directly
from transformers import AutoImageProcessor, AutoModelForImageClassification
processor = AutoImageProcessor.from_pretrained("edwinpalegre/ee8225-group4-vit-trashnet-enhanced")
model = AutoModelForImageClassification.from_pretrained("edwinpalegre/ee8225-group4-vit-trashnet-enhanced")

96
AI Training/main.py Executable file
View File

@@ -0,0 +1,96 @@
import dask.dataframe as dd
from azure.cognitiveservices.vision.customvision.training import CustomVisionTrainingClient
from azure.cognitiveservices.vision.customvision.prediction import CustomVisionPredictionClient
from azure.cognitiveservices.vision.customvision.training.models import ImageFileCreateBatch, ImageFileCreateEntry, Region
from msrest.authentication import ApiKeyCredentials
import os, time, uuid
ENDPOINT = "https://trashvision.cognitiveservices.azure.com/"
training_key = "611e786a785648e38f346f18e7f7e7ed"
prediction_key = "611e786a785648e38f346f18e7f7e7ed"
project_id = "a67f7d7b-c980-49bd-b57d-0bd1367b29d0"
credentials = ApiKeyCredentials(in_headers={"Training-key": training_key})
trainer = CustomVisionTrainingClient(ENDPOINT, credentials)
prediction_credentials = ApiKeyCredentials(in_headers={"Prediction-key": prediction_key})
predictor = CustomVisionPredictionClient(ENDPOINT, prediction_credentials)
df = dd.read_parquet("hf://datasets/edwinpalegre/trashnet_enhanced/data/train-*.parquet")
# ## iterate over the the first 5 rows of the dataframe and decode the image bytes to an image and save it to a file
tags = trainer.get_tags(project_id)
biodegradable_tag = None
cardboard_tag = None
glass_tag = None
metal_tag = None
paper_tag = None
plastic_tag = None
for tag in tags:
if tag.name == "biodegradable":
biodegradable_tag = tag
elif tag.name == "cardboard":
cardboard_tag = tag
elif tag.name == "glass":
glass_tag = tag
elif tag.name == "metal":
metal_tag = tag
elif tag.name == "paper":
paper_tag = tag
elif tag.name == "plastic":
plastic_tag = tag
print(biodegradable_tag)
print(cardboard_tag)
print(glass_tag)
print(metal_tag)
print(paper_tag)
print(plastic_tag)
# get all images from in the current dir and upload them to the custom vision project
# base_image_location = os.path.join (os.path.dirname(__file__), "images")
# tagged_images_with_regions = []
# for image in os.listdir(base_image_location):
# print(image)
# with open(os.path.join(base_image_location, image), "rb") as image_contents:
# trainer.create_images_from_data(project_id, image_contents.read(), [biodegradable_tag.id])
# print("Uploaded image: ", image)
# time.sleep(5)
skip = 10031
count = 0
for index, row in df.iterrows():
if count < skip:
count += 1
continue
else:
count += 1
image = row["image"]["bytes"]
label = row["label"]
if label == 0:
trainer.create_images_from_data(project_id, image, [biodegradable_tag.id])
elif label == 1:
trainer.create_images_from_data(project_id, image, [cardboard_tag.id])
elif label == 2:
trainer.create_images_from_data(project_id, image, [glass_tag.id])
elif label == 3:
trainer.create_images_from_data(project_id, image, [metal_tag.id])
elif label == 4:
trainer.create_images_from_data(project_id, image, [paper_tag.id])
elif label == 5:
trainer.create_images_from_data(project_id, image, [plastic_tag.id])
print(f"C: {count}, I: {index}, L: {label}, Uploaded image")
time.sleep(1)
print("Done uploading images")

39
AI Training/ml.py Executable file
View File

@@ -0,0 +1,39 @@
import dask.dataframe as dd
import os
df = dd.read_parquet("hf://datasets/edwinpalegre/trashnet_enhanced/data/train-*.parquet")
count = 0
for index, row in df.iterrows():
label = row["label"]
image = row["image"]["bytes"]
if label == 0:
with open(os.path.join("images", "biodegradable", f"biodegradable_{count}.jpg"), "wb") as f:
f.write(image)
elif label == 1:
with open(os.path.join("images", "cardboard", f"cardboard_{count}.jpg"), "wb") as f:
f.write(image)
elif label == 2:
with open(os.path.join("images", "glass", f"glass_{count}.jpg"), "wb") as f:
f.write(image)
elif label == 3:
with open(os.path.join("images", "metal", f"metal_{count}.jpg"), "wb") as f:
f.write(image)
elif label == 4:
with open(os.path.join("images", "paper", f"paper_{count}.jpg"), "wb") as f:
f.write(image)
elif label == 5:
with open(os.path.join("images", "plastic", f"plastic_{count}.jpg"), "wb") as f:
f.write(image)
else:
print("Label not found")
break
print(f"Saved image {count}")
count += 1
print("Done!")

29
AI Training/pdf.py Executable file
View File

@@ -0,0 +1,29 @@
from inference import get_model
import supervision as sv
import cv2
# define the image url to use for inference
image_file = "taylor-swift-album-1989.jpeg"
image = cv2.imread(image_file)
# load a pre-trained yolov8n model
model = get_model(model_id="taylor-swift-records/3")
# run inference on our chosen image, image can be a url, a numpy array, a PIL image, etc.
results = model.infer(image)[0]
# load the results into the supervision Detections api
detections = sv.Detections.from_inference(results)
# create supervision annotators
bounding_box_annotator = sv.BoundingBoxAnnotator()
label_annotator = sv.LabelAnnotator()
# annotate the image with our inference results
annotated_image = bounding_box_annotator.annotate(
scene=image, detections=detections)
annotated_image = label_annotator.annotate(
scene=annotated_image, detections=detections)
# display the image
sv.plot_image(annotated_image)

29
AI Training/train.py Executable file
View File

@@ -0,0 +1,29 @@
import dask.dataframe as dd
from azure.cognitiveservices.vision.customvision.training import CustomVisionTrainingClient
from azure.cognitiveservices.vision.customvision.prediction import CustomVisionPredictionClient
from azure.cognitiveservices.vision.customvision.training.models import ImageFileCreateBatch, ImageFileCreateEntry, Region
from msrest.authentication import ApiKeyCredentials
import os, time, uuid
ENDPOINT = "https://trashvision.cognitiveservices.azure.com/"
training_key = "611e786a785648e38f346f18e7f7e7ed"
prediction_key = "611e786a785648e38f346f18e7f7e7ed"
project_id = "a67f7d7b-c980-49bd-b57d-0bd1367b29d0"
credentials = ApiKeyCredentials(in_headers={"Training-key": training_key})
trainer = CustomVisionTrainingClient(ENDPOINT, credentials)
prediction_credentials = ApiKeyCredentials(in_headers={"Prediction-key": prediction_key})
predictor = CustomVisionPredictionClient(ENDPOINT, prediction_credentials)
print ("Training...")
iteration = trainer.train_project(project_id)
while (iteration.status != "Completed"):
iteration = trainer.get_iteration(project_id, iteration.id)
print ("Training status: " + iteration.status)
time.sleep(1)
# The iteration is now trained. Publish it to the project endpoint
trainer.publish_iteration(project_id, iteration.id, "HaxNet1", predictor)
print ("Done!")

38
File Converter/index.js Executable file
View File

@@ -0,0 +1,38 @@
import express from 'express';
import bodyParser from 'body-parser';
import fs from 'fs';
import path from 'path';
import fileUpload from 'express-fileupload';
import { pdf } from "pdf-to-img";
const app = express();
app.use(bodyParser.json());
app.use(fileUpload());
app.post('/convert', async(req, res) => {
console.log(req.files);
let file = req.files.pdf;
let images = [];
const document = await pdf(file.data, { scale: 3 });
for await (const image of document) {
images.push(image);
}
res.send(images);
});
app.get('/', (req, res) => {
res.send('Hello World');
});
app.listen(5000, () => {
console.log('Server is running on http://localhost:3000');
});

21
File Converter/package.json Executable file
View File

@@ -0,0 +1,21 @@
{
"name": "Opensource",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.20.3",
"express": "^4.21.1",
"express-fileupload": "^1.5.1",
"fs": "0.0.1-security",
"path": "^0.12.7",
"pdf-to-img": "^4.1.1"
}
}

32
Project/.dockerignore Executable file
View File

@@ -0,0 +1,32 @@
# Ignore node_modules directory
node_modules
# Ignore npm debug log
npm-debug.log
# Ignore Dockerfile and .dockerignore file
Dockerfile
.dockerignore
# Ignore build output
/build
/dist
# Ignore logs
logs
*.log
# Ignore temporary files
tmp
temp
*.tmp
*.temp
# Ignore coverage directory
coverage
# Ignore .git directory and related files
.git
.gitignore

20
Project/.eslintignore Executable file
View File

@@ -0,0 +1,20 @@
.now/*
*.css
.changeset
dist
esm/*
public/*
tests/*
scripts/*
*.config.js
.DS_Store
node_modules
coverage
.next
build
!.commitlintrc.cjs
!.lintstagedrc.cjs
!jest.config.js
!plopfile.js
!react-shim.js
!tsup.config.ts

122
Project/.eslintrc.json Executable file
View File

@@ -0,0 +1,122 @@
{
"$schema": "https://json.schemastore.org/eslintrc.json",
"env": {
"browser": false,
"es2021": true,
"node": true
},
"extends": [
"next",
"plugin:react/recommended",
"plugin:prettier/recommended",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended"
],
"plugins": [
"react",
"unused-imports",
"import",
"@typescript-eslint",
"jsx-a11y",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"react/no-unescaped-entities": "off",
"@next/next/no-page-custom-font": "off",
"no-console": "warn",
"react/prop-types": "off",
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"react-hooks/exhaustive-deps": "off",
"jsx-a11y/click-events-have-key-events": "warn",
"jsx-a11y/interactive-supports-focus": "warn",
"prettier/prettier": "off",
"no-unused-vars": "off",
"unused-imports/no-unused-vars": "off",
"unused-imports/no-unused-imports": "warn",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"args": "after-used",
"ignoreRestSiblings": false,
"argsIgnorePattern": "^_.*?$"
}
],
"import/order": [
"warn",
{
"groups": [
"type",
"builtin",
"object",
"external",
"internal",
"parent",
"sibling",
"index"
],
"pathGroups": [
{
"pattern": "~/**",
"group": "external",
"position": "after"
}
],
"newlines-between": "always"
}
],
"react/self-closing-comp": "warn",
"react/jsx-sort-props": [
"warn",
{
"callbacksLast": true,
"shorthandFirst": true,
"noSortAlphabetically": false,
"reservedFirst": true
}
],
"padding-line-between-statements": [
"warn",
{
"blankLine": "always",
"prev": "*",
"next": "return"
},
{
"blankLine": "always",
"prev": [
"const",
"let",
"var"
],
"next": "*"
},
{
"blankLine": "any",
"prev": [
"const",
"let",
"var"
],
"next": [
"const",
"let",
"var"
]
}
]
}
}

38
Project/.gitignore vendored Executable file
View File

@@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
bun.lockb
.env

25
Project/Dockerfile Executable file
View File

@@ -0,0 +1,25 @@
# Use the official Node.js image as the base image
FROM node:20
# Set the working directory
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm install bun -g
RUN bun install
# Copy the rest of the application code
COPY . .
# Build the Next.js application
RUN bun run build
# Expose the port the app runs on
EXPOSE 3000
# Start the Next.js application
CMD ["bun", "start"]

1
Project/README.md Executable file
View File

@@ -0,0 +1 @@
An app for tracking the carbon footprint of buildings.

View File

@@ -0,0 +1,100 @@
// @ts-nocheck
import { NextResponse } from 'next/server';
import { CosmosClient } from "@azure/cosmos";
const cosmosClient = new CosmosClient({
endpoint: process.env.COSMOS_ENDPOINT!,
key: process.env.COSMOS_KEY!
});
const database = cosmosClient.database(process.env.COSMOS_DATABASE_ID!);
const container = database.container(process.env.COSMOS_CONTAINER_ID!);
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
console.log("Received GET request with id:", id);
try {
if (id) {
// Get a single building
console.log("Attempting to get building with id:", id);
const querySpec = {
query: "SELECT * FROM c WHERE c.id = @id",
parameters: [{ name: "@id", value: id }]
};
const { resources } = await container.items.query(querySpec).fetchAll();
console.log("Query result:", resources);
if (resources && resources.length > 0) {
console.log("Returning resource for id:", id);
return NextResponse.json(resources[0]);
} else {
console.log("Building not found for id:", id);
return NextResponse.json({ message: "Building not found" }, { status: 404 });
}
} else {
// Get all buildings
console.log("Attempting to get all buildings");
const { resources } = await container.items.readAll().fetchAll();
console.log("Number of buildings retrieved:", resources.length);
return NextResponse.json(resources);
}
} catch (error) {
console.error("Error in GET request:", error);
return NextResponse.json({ message: "Error fetching data", error }, { status: 500 });
}
}
// function deepMerge(target: any, source: any) {
// for (const key in source) {
// if (Array.isArray(source[key])) {
// if (!target[key]) target[key] = [];
// target[key] = [...target[key], ...source[key]];
// } else if (source[key] instanceof Object && key in target) {
// deepMerge(target[key], source[key]);
// } else {
// target[key] = source[key];
// }
// }
// return target;
// }
export async function PATCH(request: Request) {
try {
const { id, operation, ...data } = await request.json();
// Query for the existing item
const querySpec = {
query: "SELECT * FROM c WHERE c.id = @id",
parameters: [{ name: "@id", value: id }]
};
const { resources } = await container.items.query(querySpec).fetchAll();
let existingItem = resources[0] || { id };
if (operation === 'deleteWasteEntry') {
// Remove the waste entry at the specified index
const index = data.index;
existingItem.wasteGeneration.splice(index, 1);
} else {
// Deep merge the existing data with the new data
existingItem = { ...existingItem, ...data };
}
// Upsert the item
const { resource: result } = await container.items.upsert(existingItem);
console.log("Update successful. Result:", result);
return NextResponse.json({ message: "Building updated successfully", result });
} catch (error) {
return NextResponse.json({ message: "Error updating data", error }, { status: 500 });
}
}

74
Project/app/api/chat/route.ts Executable file
View File

@@ -0,0 +1,74 @@
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
try {
const { imageURL, type } = await request.json();
if (!imageURL) {
return NextResponse.json({ error: "No image URL provided" }, { status: 400 });
}
const payload = {
messages: [
{
role: "user",
content: [
{
type: "text",
text: ` Analyze the following ${type} bill image and extract the following information:
1. Multiple data points of usage, each with a date and ${type === 'gas' ? 'therms' : 'kWh'} used
2. Any other relevant usage data
Format the output as a JSON object with an array of data points and any additional data.
You must output valid JSON in the following format, or an empty array if no data is found:
{
"dataPoints": [
{
"date": "<ISO 8601 date string>",
"usage": <number>
},
// ... more data points
]
}
`
},
{
type: "image_url",
image_url: {
url: imageURL
}
}
]
}
],
temperature: 0.4,
top_p: 0.95,
max_tokens: 1000
};
const response = await fetch(process.env.AZURE_OPENAI_ENDPOINT as string, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'api-key': process.env.AZURE_OPENAI_KEY as string,
},
body: JSON.stringify(payload),
});
console.log('CHAT RESPONSE', response);
if (!response.ok) {
throw new Error('Failed to generate description: ' + response.status + " " + response.statusText);
}
const data = await response.json();
const description = data.choices[0].message.content;
console.log("CHAT DESCRIPTION", description);
return NextResponse.json({ response: description });
} catch (error) {
console.error('Error processing chat:', error);
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
}
}

View File

@@ -0,0 +1,43 @@
// @ts-nocheck
import { NextRequest, NextResponse } from 'next/server';
import { toBase64 } from 'openai/core';
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
let res = await fetch(process.env.PDF_URI, {
method: 'POST',
body: formData
});
res = await res.json();
const pdfBuffer = await res[0];
let b64 = await toBase64(pdfBuffer);
console.log(b64);
console.log(request);
// Step 2: Use the image with the chat route
const chatResponse = await fetch(process.env.PROD_URL + '/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
imageURL: `data:image/png;base64,${b64}`,
type: formData.get('type'),
}),
});
const chatData = await chatResponse.json();
console.log("CHAT RESPONSE", chatData);
return NextResponse.json({ message: 'PDF converted successfully', response: chatData });
} catch (error) {
console.error('Error processing PDF:', error);
return NextResponse.json({ error: 'Failed to process PDF' }, { status: 500 });
}
}

View File

@@ -0,0 +1,217 @@
// app/buildings/[buildingid]/emissions/page.tsx
"use client";
import { useState, useEffect } from "react";
import { CalendarDate } from "@internationalized/date";
import { Button } from "@nextui-org/button";
import { Divider } from "@nextui-org/divider";
import { ButtonGroup } from "@nextui-org/button";
import { Card, CardHeader, CardBody } from "@nextui-org/react";
import { Input } from "@nextui-org/input";
import { Popover, PopoverTrigger, PopoverContent } from "@nextui-org/popover";
import { Calendar, DateValue } from "@nextui-org/calendar";
import AddDataButton from "@/components/addDataButton";
import { useBuilding } from "@/lib/useBuildingData";
import EmissionsGraph from "@/components/emissionsGraph";
interface EmissionsPageProps {
params: { buildingid: string };
}
export default function EmissionsPage({ params }: EmissionsPageProps) {
const { data: buildingData } = useBuilding(params.buildingid);
// State for filters
const [startDate, setStartDate] = useState<DateValue | null>(null);
const [endDate, setEndDate] = useState<DateValue | null>(null);
const [showWaste, setShowWaste] = useState(true);
const [showElectricity, setShowElectricity] = useState(true);
const [showGas, setShowGas] = useState(true);
const [graphType, setGraphType] = useState<'line' | 'area' | 'pie'>('line');
useEffect(() => {
if (buildingData) {
const allDates = [
...buildingData.electricityUsage.map(d => d.timestamp),
...buildingData.naturalGasUsage.map(d => d.timestamp),
...buildingData.wasteGeneration.map(d => d.timestamp)
];
if (allDates.length > 0) {
const earliestDate = new Date(Math.min(...allDates.map(d => (d as any).seconds * 1000)));
const latestDate = new Date(Math.max(...allDates.map(d => (d as any).seconds * 1000)));
earliestDate.setDate(earliestDate.getDate() - 1);
latestDate.setDate(latestDate.getDate() + 1);
setStartDate(new CalendarDate(earliestDate.getFullYear(), earliestDate.getMonth() + 1, earliestDate.getDate()));
setEndDate(new CalendarDate(latestDate.getFullYear(), latestDate.getMonth() + 1, latestDate.getDate()));
}
}
}, [buildingData]);
const handlePdfToImage = async () => {
try {
const formData = new FormData();
const pdfResponse = await fetch('/electricity-sample-bill.pdf');
const pdfBlob = await pdfResponse.blob();
formData.append('pdf', pdfBlob, 'electricity-sample-bill.pdf');
const response = await fetch('/api/pdf-to-image', {
method: 'POST',
body: { ...formData, type: 'electricity' }
});
if (!response.ok) {
throw new Error('Failed to convert PDF to image');
}
const result = await response.json();
console.log('PDF to Image conversion result:', result);
// Handle the result as needed
} catch (error) {
console.error('Error converting PDF to image:', error);
// Handle the error (e.g., show an error message to the user)
}
};
const handleStartDateChange = (date: DateValue) => {
setStartDate(date);
};
const handleEndDateChange = (date: DateValue) => {
setEndDate(date);
};
return (
<div className="flex flex-col items-center h-full p-4">
{/* Tab Title */}
<h1 className="text-6xl text-left self-start font-bold pt-8">
{`Emissions`}
</h1>
{/* Group for filters plus graph */}
<div className="flex flex-col justify-center w-full h-full">
{/* Horizontal group for adding data and filters */}
<AddDataButton buildingid={params.buildingid} />
<div className="flex gap-4 mt-4">
{/* Data Type Selection Card */}
<Card className="flex-1">
<CardHeader>
<h3 className="text-lg font-semibold">Data Types</h3>
</CardHeader>
<CardBody>
<div className="flex gap-2">
<Button
color={showElectricity ? "primary" : "default"}
onClick={() => setShowElectricity(!showElectricity)}
>
Electricity
</Button>
<Button
color={showGas ? "primary" : "default"}
onClick={() => setShowGas(!showGas)}
>
Natural Gas
</Button>
<Button
color={showWaste ? "primary" : "default"}
onClick={() => setShowWaste(!showWaste)}
>
Waste
</Button>
</div>
</CardBody>
</Card>
{/* Chart Type Selection Card */}
<Card className="flex-1">
<CardHeader>
<h3 className="text-lg font-semibold">Chart Type</h3>
</CardHeader>
<CardBody>
<ButtonGroup>
<Button
color={graphType === 'line' ? "primary" : "default"}
onClick={() => setGraphType('line')}
>
Line
</Button>
<Button
color={graphType === 'area' ? "primary" : "default"}
onClick={() => setGraphType('area')}
>
Area
</Button>
<Button
color={graphType === 'pie' ? "primary" : "default"}
onClick={() => setGraphType('pie')}
>
Pie
</Button>
</ButtonGroup>
</CardBody>
</Card>
{/* Date Range Selection Card */}
<Card className="flex-1">
<CardHeader>
<h3 className="text-lg font-semibold">Date Range</h3>
</CardHeader>
<CardBody>
<div className="flex gap-2">
<Popover placement="bottom">
<PopoverTrigger>
<Input
readOnly
label="Start Date"
value={startDate ? startDate.toString() : ''}
/>
</PopoverTrigger>
<PopoverContent>
<Calendar
showMonthAndYearPickers
value={startDate}
onChange={handleStartDateChange}
/>
</PopoverContent>
</Popover>
<Popover placement="bottom">
<PopoverTrigger>
<Input
readOnly
label="End Date"
value={endDate ? endDate.toString() : ''}
/>
</PopoverTrigger>
<PopoverContent>
<Calendar
showMonthAndYearPickers
value={endDate}
onChange={handleEndDateChange}
/>
</PopoverContent>
</Popover>
</div>
</CardBody>
</Card>
</div>
<Divider className="mt-6" />
{/* Render emissions graph */}
<EmissionsGraph
buildingid={params.buildingid}
filters={{ startDate: startDate ? startDate.toDate('UTC') : null, endDate: endDate ? endDate.toDate('UTC') : null, showWaste, showElectricity, showGas }}
graphType={graphType}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,17 @@
// app/buildings/[buildingid]/layout.tsx
import Sidebar from "../../../components/sidebar";
export default function BuildingLayout({
children,
params
}: {
children: React.ReactNode;
params: { buildingid: string };
}) {
return (
<div className="flex h-screen w-full">
<Sidebar buildingid={params.buildingid} />
<main className="flex-1 max-h-screen overflow-y-auto">{children}</main>
</div>
);
}

View File

@@ -0,0 +1,13 @@
// app/buildings/[buildingid]/page.tsx
interface BuildingPageProps {
params: { buildingid: string };
}
export default function BuildingPage({ params }: BuildingPageProps) {
return (
<div className="flex items-center justify-center text-center h-full">
Select a tab to view information about this building.
</div>
);
}

View File

@@ -0,0 +1,136 @@
"use client";
import React, { useRef, useState, useCallback, useEffect } from 'react';
import Webcam from 'react-webcam';
import { Card, CardBody } from '@nextui-org/card';
import { Button } from '@nextui-org/button';
import screenfull from 'screenfull';
const TrashScanner: React.FC = () => {
const webcamRef = useRef<Webcam>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [isDrawing, setIsDrawing] = useState(false);
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
const [endPos, setEndPos] = useState({ x: 0, y: 0 });
const containerRef = useRef<HTMLDivElement>(null);
//canvas size
const setCanvasSize = useCallback(() => {
const video = webcamRef.current?.video;
const canvas = canvasRef.current;
if (video && canvas) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
}
}, []);
// listener to set canvas size when video is ready
useEffect(() => {
const video = webcamRef.current?.video;
if (video) {
video.addEventListener('loadedmetadata', setCanvasSize);
}
return () => {
if (video) {
video.removeEventListener('loadedmetadata', setCanvasSize);
}
};
}, [setCanvasSize]);
const handleMouseDown = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
setIsDrawing(true);
setStartPos({ x, y });
setEndPos({ x, y });
}, []);
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
if (!isDrawing) return;
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
setEndPos({ x, y });
}, [isDrawing]);
const handleMouseUp = useCallback(() => {
setIsDrawing(false);
}, []);
const drawBox = useCallback(() => {
const canvas = canvasRef.current;
const ctx = canvas?.getContext('2d');
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
ctx.strokeRect(
Math.min(startPos.x, endPos.x),
Math.min(startPos.y, endPos.y),
Math.abs(endPos.x - startPos.x),
Math.abs(endPos.y - startPos.y)
);
}, [startPos, endPos]);
React.useEffect(() => {
drawBox();
}, [drawBox]);
const toggleFullScreen = useCallback(() => {
if (containerRef.current && screenfull.isEnabled) {
screenfull.toggle(containerRef.current);
}
}, []);
return (
<div className="container-fluid p-4">
<h1 className="text-2xl font-bold text-center mb-4">Trash Scanner</h1>
<div className="flex flex-col items-center">
<Card className="w-full md:w-auto md:max-w-[640px] mb-4">
<CardBody className="p-0">
<div ref={containerRef} className="relative aspect-video">
<Webcam
audio={false}
ref={webcamRef}
screenshotFormat="image/jpeg"
className="w-full h-full object-cover rounded-lg"
/>
<canvas
ref={canvasRef}
className="absolute top-0 left-0 w-full h-full rounded-lg"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
/>
</div>
</CardBody>
</Card>
<Button
onClick={toggleFullScreen}
className="w-full md:w-auto md:max-w-[640px]"
>
Toggle Fullscreen
</Button>
</div>
</div>
);
};
export default TrashScanner;

View File

@@ -0,0 +1,7 @@
// app/buildings/[buildingid]/trash-scanner/page.tsx
import RealtimeModel from "@/components/trashDetection";
export default function TrashScanner() {
return <RealtimeModel />;
};

View File

@@ -0,0 +1,177 @@
"use client";
import { useParams } from "next/navigation";
import { useState } from "react";
import { Button } from "@nextui-org/button";
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@nextui-org/modal";
import { Input } from "@nextui-org/input";
import { Timestamp } from "firebase/firestore";
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from "@nextui-org/table";
import { Select, SelectItem } from "@nextui-org/react";
import { useBuilding, WasteDataPoint } from "@/lib/useBuildingData";
import { trashItems } from "@/components/trashcanMode";
export default function TrashPage() {
const { buildingid } = useParams();
const { data: building, isLoading, error, updateBuilding } = useBuilding(buildingid as string);
const [isModalOpen, setIsModalOpen] = useState(false);
const [newEntry, setNewEntry] = useState({
timestamp: new Date().toISOString().slice(0, 16),
type: "",
trashcanID: "",
wasteCategory: "",
emissions: 0,
});
const [sortConfig, setSortConfig] = useState<{ key: keyof WasteDataPoint; direction: 'ascending' | 'descending' } | null>(null);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!building) return <div>Building not found</div>;
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
if (name === 'emissions') {
const inputValue = parseFloat(value);
const scaledValue = isNaN(inputValue) ? 0 : inputValue / 1e+3;
setNewEntry(prev => ({ ...prev, [name]: scaledValue }));
} else {
setNewEntry(prev => ({ ...prev, [name]: value }));
}
};
const handleSubmit = () => {
const updatedWasteGeneration = [
...building!.wasteGeneration,
{ ...newEntry, timestamp: Timestamp.fromDate(new Date(newEntry.timestamp)), emissions: Number(newEntry.emissions) }
];
updateBuilding({ wasteGeneration: updatedWasteGeneration as WasteDataPoint[] });
setIsModalOpen(false);
};
const handleSort = (key: keyof WasteDataPoint) => {
let direction: 'ascending' | 'descending' = 'ascending';
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') {
direction = 'descending';
}
setSortConfig({ key, direction });
};
const sortedWasteGeneration = [...building.wasteGeneration].sort((a, b) => {
if (!sortConfig) return 0;
const { key, direction } = sortConfig;
if (a[key] < b[key]) return direction === 'ascending' ? -1 : 1;
if (a[key] > b[key]) return direction === 'ascending' ? 1 : -1;
return 0;
});
const handleDelete = (index: number) => {
updateBuilding({ operation: 'deleteWasteEntry', index });
};
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Waste Data for {building?.name}</h1>
<Table aria-label="Waste data table">
<TableHeader>
<TableColumn key="timestamp" onClick={() => handleSort('timestamp')}>Timestamp</TableColumn>
<TableColumn key="wasteCategory" onClick={() => handleSort('wasteCategory')}>Name</TableColumn>
<TableColumn key="type" onClick={() => handleSort('type')}>Trash Category</TableColumn>
<TableColumn key="trashcanID" onClick={() => handleSort('trashcanID')}>Trashcan ID</TableColumn>
<TableColumn key="emissions" onClick={() => handleSort('emissions')}>Emissions (kg ofCO2e)</TableColumn>
<TableColumn key="actions">Actions</TableColumn>
</TableHeader>
<TableBody>
{sortedWasteGeneration.map((wastePoint, index) => (
<TableRow key={index}>
<TableCell>{new Date(wastePoint.timestamp.seconds * 1000).toLocaleString()}</TableCell>
<TableCell>{wastePoint.wasteCategory}</TableCell>
<TableCell>{wastePoint.type}</TableCell>
<TableCell>{wastePoint.trashcanID}</TableCell>
<TableCell>{(wastePoint.emissions * 1e+3).toFixed(0)}</TableCell>
<TableCell>
<Button color="danger" size="sm" onPress={() => handleDelete(index)}>Delete</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Button className="mt-4" onPress={() => setIsModalOpen(true)}>
Add New Entry
</Button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<ModalContent>
<ModalHeader>
<h2 className="text-lg font-semibold">Add New Waste Entry</h2>
</ModalHeader>
<ModalBody>
<Input
label="Timestamp"
name="timestamp"
type="datetime-local"
value={newEntry.timestamp}
onChange={handleInputChange}
/>
<Select
className="w-full"
label="Type"
name="type"
selectedKeys={[newEntry.type]}
onChange={handleInputChange}
>
{trashItems.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.name}
</SelectItem>
))}
</Select>
<Input
label="Trashcan ID"
name="trashcanID"
value={newEntry.trashcanID}
onChange={handleInputChange}
/>
<Select
label="Waste Category"
name="wasteCategory"
selectedKeys={[newEntry.wasteCategory]}
onChange={handleInputChange}
>
<SelectItem key="Landfill" value="Landfill">
Landfill
</SelectItem>
<SelectItem key="Recycling" value="Recycling">
Recycling
</SelectItem>
<SelectItem key="Compost" value="Compost">
Compost
</SelectItem>
</Select>
<Input
label="Emissions (grams of CO2e)"
name="emissions"
type="number"
value={(newEntry.emissions * 1e+3).toString()} // Multiply by 1e+3 for display
onChange={handleInputChange}
/>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={() => setIsModalOpen(false)}>
Cancel
</Button>
<Button color="primary" onPress={handleSubmit}>
Add Entry
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</div>
);
}

View File

@@ -0,0 +1,7 @@
// app/buildings/[buildingid]/trashcan-mode/page.tsx
import TrashcanMode from "@/components/trashcanMode";
export default function TrashcanModePage() {
return <TrashcanMode />;
}

61
Project/app/buildings/page.tsx Executable file
View File

@@ -0,0 +1,61 @@
// app/buildings/page.tsx
"use client";
import { Card, CardHeader, CardFooter, Image, Button, Skeleton } from "@nextui-org/react";
import Link from "next/link";
import { useBuildingList } from "@/lib/useBuildingData";
export default function BuildingsPage() {
const { data: buildings, isLoading, error } = useBuildingList();
if (isLoading) return (
<div className="grid grid-cols-12 gap-4 p-4">
{[...Array(3)].map((_, index) => (
<Card key={index} className="w-full h-[300px] col-span-12 sm:col-span-6 md:col-span-4">
<Skeleton className="rounded-lg">
<div className="h-[300px]" />
</Skeleton>
</Card>
))}
</div>
);
if (error) return <div>Error: {error.message}</div>;
if (buildings) return (
<div className="grid grid-cols-12 gap-4 p-4 h-full bg-orange-900/5">
{buildings.map(building => (
<Card
key={building.id}
isFooterBlurred
className="w-full h-[300px] col-span-12 sm:col-span-6 md:col-span-4"
>
<CardHeader className="absolute z-10 top-1 flex-col items-start bg-gray-800/5 backdrop-blur-lg rounded-none -mt-1">
<h4 className="text-white font-medium text-2xl">{building.name}</h4>
<p className="text-white/60 text-small">{building.address}</p>
</CardHeader>
<Image
removeWrapper
alt={`${building.name} image`}
className="z-0 w-full h-full object-cover"
src={building.imageURL}
/>
<CardFooter className="absolute bg-black/40 bottom-0 z-10 justify-between">
<div>
<p className="text-white text-tiny">Year Built: {building.yearBuilt}</p>
<p className="text-white text-tiny">Square Footage: {building.squareFeet.toLocaleString()}</p>
</div>
<Link href={`/buildings/${building.id}/emissions`}>
<Button className="text-tiny" color="primary" radius="full" size="sm">
View Building
</Button>
</Link>
</CardFooter>
</Card>
))}
</div>
);
return <div>No buildings found</div>;
}

31
Project/app/error.tsx Executable file
View File

@@ -0,0 +1,31 @@
"use client";
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
// Log the error to an error reporting service
/* eslint-disable no-console */
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Try again
</button>
</div>
);
}

24
Project/app/featureBox.tsx Executable file
View File

@@ -0,0 +1,24 @@
import React from 'react';
interface FeatureBoxProps {
title: string;
description: string;
theme?: string;
}
const FeatureBox: React.FC<FeatureBoxProps> = ({ title, description, theme }) => {
const isDarkMode = theme === 'dark';
return (
<div className={`p-6 rounded-lg shadow-md ${isDarkMode ? 'bg-gray-800' : 'bg-white'}`}>
<h3 className={`text-xl font-bold mb-2 ${isDarkMode ? 'text-white' : 'text-gray-800'}`}>
{title}
</h3>
<p className={isDarkMode ? 'text-gray-300' : 'text-gray-600'}>
{description}
</p>
</div>
);
};
export default FeatureBox;

54
Project/app/layout.tsx Executable file
View File

@@ -0,0 +1,54 @@
import "@/styles/globals.css";
import { Metadata, Viewport } from "next";
import clsx from "clsx";
import { Providers } from "./providers";
import { siteConfig } from "@/config/site";
import { fontSans } from "@/config/fonts";
import { Navbar } from "@/components/navbar";
export const metadata: Metadata = {
title: {
default: siteConfig.name,
template: `%s - ${siteConfig.name}`,
},
description: siteConfig.description,
icons: {
icon: "/favicon.ico",
},
};
export const viewport: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" },
],
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html suppressHydrationWarning lang="en">
<head />
<body
className={clsx(
"min-h-screen bg-background font-sans antialiased",
fontSans.variable,
)}
>
<Providers themeProps={{ attribute: "class", defaultTheme: "light" }}>
<div className="flex flex-col min-h-screen">
<Navbar />
<main className="flex-1">
{children}
</main>
</div>
</Providers>
</body>
</html>
);
}

53
Project/app/mission/page.tsx Executable file
View File

@@ -0,0 +1,53 @@
'use client'
import Image from 'next/image';
import { Button } from "@nextui-org/react";
import Link from "next/link";
export default function MissionPage() {
return (
<div className="relative min-h-screen bg-orange-900/5">
{/* Large "About Us" text at the top */}
<div className="absolute top-0 left-0 right-0 z-10 flex justify-center items-center h-40">
<h1 className="text-6xl font-bold bg-gradient-to-r from-[#FF705B] to-[#FFB457] text-transparent bg-clip-text drop-shadow-lg">Our Mission</h1>
</div>
<div className="relative min-h-screen">
<Image
src="/demo.png"
alt="Demo image"
width={1920}
height={3000}
className="w-full h-auto"
/>
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-background via-background to-transparent h-80 dark:from-background dark:via-background">
{/* text at the bottom */}
<div className="flex flex-col self-end items-center pb-16">
<p className="mt-40 text-lg text-center max-w-2xl">
One of the most neglected aspects of a building&apos;s carbon footprint is waste mismanagement and poor recycling practices. According to the EPA, landfills account for <span className="bg-gradient-to-r from-[#FF705B] to-[#FFB457] text-transparent bg-clip-text">15% of U.S. methane emissions</span>, with commercial buildings generating over <span className="bg-gradient-to-r from-[#FF705B] to-[#FFB457] text-transparent bg-clip-text">30% of the nation&apos;s total waste</span>.
<br />
<br />
Studies show that up to <span className="bg-gradient-to-r from-[#FF705B] to-[#FFB457] text-transparent bg-clip-text">25% of items</span> in recycling bins are actually non-recyclable, contaminating entire loads. Only 32% of commercial waste is recycled, compared to a potential 75% that could be. Proper recycling can reduce a building&apos;s carbon emissions <span className="bg-gradient-to-r from-[#FF705B] to-[#FFB457] text-transparent bg-clip-text">by up to 40%</span>.
<br />
<br />
<strong>Carbin</strong> uses a machine learning algorithm to identify the type of waste at the trash chute, nudging the occupants to recycle correctly with a friendly reminder. Our long term goal is to <span className="bg-gradient-to-r from-[#FF705B] to-[#FFB457] text-transparent bg-clip-text">educate building occupants</span>, something we know will truly revolutionize waste management, make efficient sorting and recycling the norm, and significantly curtail the carbon impact of our daily operations.
</p>
<Button
as={Link}
href="/buildings"
variant="solid"
size="lg"
endContent={
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
}
className="mb-2 mt-6 bg-orange-500 text-white"
>
Intrigued? Explore participating buildings
</Button>
</div>
</div>
</div>
</div >
);
}

86
Project/app/page.tsx Executable file
View File

@@ -0,0 +1,86 @@
'use client';
import { Link } from "@nextui-org/link";
import { button as buttonStyles } from "@nextui-org/theme";
import { useTheme } from "next-themes";
import { title, subtitle } from "@/components/primitives";
import FeatureBox from "@/app/featureBox";
export default function Home() {
const { theme } = useTheme();
return (
<div className="min-h-screen flex flex-col">
<section className="flex-grow flex flex-col items-center justify-center gap-4 py-8 md:py-10 overflow-hidden relative">
<div className="absolute inset-0 z-0">
<video
autoPlay
loop
muted
playsInline
className="object-cover w-full h-full"
>
<source src="/homepage-video.mp4" type="video/mp4" />
<a href="https://www.vecteezy.com/free-videos/city-time-lapse-at-night">City Time Lapse At Night Stock Videos by Vecteezy</a>
</video>
<div className="absolute inset-0 bg-orange-900 opacity-60" />
</div>
<div className="relative z-10 text-left w-full max-w-4xl px-6">
<div className="mb-4">
<span className={title({ class: "text-white" })}>
Your platform for{" "}
</span>
<span className={title({ color: "yellow" })}>
building
</span>
<br />
<span className={title({ class: "text-white" })}>
a sustainable future
</span>
</div>
<div className={subtitle({ class: "mt-4 text-gray-200" })}>
Encourage <span className="text-orange-400">student participation</span> in responsible waste management with smart bins that guide proper disposal.
</div>
<div className="mt-8">
<Link
className={buttonStyles({
color: "warning",
radius: "full",
variant: "shadow",
size: "lg",
})}
href="/buildings"
>
Get Started
</Link>
</div>
</div>
</section>
<div className="w-full bg-white dark:bg-gray-900 py-16">
<div className="max-w-4xl mx-auto px-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<FeatureBox
title="Smart Bins with Real-time Feedback"
description="Utilize our AI-powered smart bins to guide students on proper waste disposal, and get immediate feedback."
theme={theme}
/>
<FeatureBox
title="Track Waste and Emissions"
description="Log your building's trash and monitor emissions over time, giving you insights into your waste management efficiency."
theme={theme}
/>
<FeatureBox
title="Measure Your Impact"
description="Track your building's emissions reduction and see the emissions saved through our smart bin system."
theme={theme}
/>
</div>
</div>
</div>
</div>
);
}

26
Project/app/providers.tsx Executable file
View File

@@ -0,0 +1,26 @@
"use client";
import * as React from "react";
import { NextUIProvider } from "@nextui-org/system";
import { useRouter } from "next/navigation";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes/dist/types";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
export interface ProvidersProps {
children: React.ReactNode;
themeProps?: ThemeProviderProps;
}
export function Providers({ children, themeProps }: ProvidersProps) {
const router = useRouter();
const [queryClient] = React.useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<NextUIProvider navigate={router.push}>
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
</NextUIProvider>
</QueryClientProvider>
);
}

View File

@@ -0,0 +1,37 @@
// components/addDataButton.tsx
"use client";
import { useState } from "react";
import { Button } from "@nextui-org/react";
import { PlusIcon } from "./icons";
import { UploadDataModal } from "./uploadDataModal";
import { useBuilding } from "@/lib/useBuildingData";
interface AddDataButtonProps {
buildingid: string;
}
export default function AddDataButton({ buildingid }: AddDataButtonProps) {
const { updateBuilding } = useBuilding(buildingid);
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<>
<Button
className="w-fit"
startContent={<PlusIcon size={16} />}
onPress={() => setIsModalOpen(true)}
>
Upload new data
</Button>
<UploadDataModal
buildingid={buildingid}
isOpen={isModalOpen}
updateBuilding={updateBuilding}
onClose={() => setIsModalOpen(false)}
/>
</>
);
}

View File

@@ -0,0 +1,218 @@
// components/emissionsGraph.tsx
import React, { useMemo } from 'react';
import { LineChart, Line, AreaChart, Area, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { useBuilding, Building, ElectricityDataPoint, NaturalGasDataPoint, WasteDataPoint } from '@/lib/useBuildingData';
export type EmissionGraphFilters = {
startDate?: Date | null;
endDate?: Date | null;
showWaste?: boolean;
showElectricity?: boolean;
showGas?: boolean;
}
interface EmissionsGraphProps {
buildingid: string;
filters: EmissionGraphFilters;
graphType: 'line' | 'area' | 'pie';
}
type ChartDataPoint = {
date: string;
electricity: number;
gas: number;
waste: number;
};
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8'];
export default function EmissionsGraph({ buildingid, filters, graphType }: EmissionsGraphProps) {
const { data: building, isLoading, error } = useBuilding(buildingid);
const chartData = useMemo(() => {
if (!building) return [];
const dataMap = new Map<string, Partial<ChartDataPoint>>();
const addDataPoint = (date: Date, type: 'electricity' | 'gas' | 'waste', value: number) => {
const dateString = date.toISOString().split('T')[0];
const existingData = dataMap.get(dateString) || { date: dateString };
existingData[type] = value;
dataMap.set(dateString, existingData);
};
// Collect all unique dates and data points
const allDates = new Set<string>();
const typedDataPoints: { [key: string]: { date: string, value: number }[] } = {
electricity: [],
gas: [],
waste: []
};
if (filters.showElectricity) {
building.electricityUsage.forEach((point: ElectricityDataPoint) => {
const date = new Date(point.timestamp.seconds * 1000);
const dateString = date.toISOString().split('T')[0];
allDates.add(dateString);
typedDataPoints.electricity.push({ date: dateString, value: point.emissions });
});
}
if (filters.showGas) {
building.naturalGasUsage.forEach((point: NaturalGasDataPoint) => {
const date = new Date(point.timestamp.seconds * 1000);
const dateString = date.toISOString().split('T')[0];
allDates.add(dateString);
typedDataPoints.gas.push({ date: dateString, value: point.emissions });
});
}
if (filters.showWaste) {
building.wasteGeneration.forEach((point: WasteDataPoint) => {
const date = new Date(point.timestamp.seconds * 1000);
const dateString = date.toISOString().split('T')[0];
allDates.add(dateString);
typedDataPoints.waste.push({ date: dateString, value: point.emissions });
});
}
// Sort dates and data points
const sortedDates = Array.from(allDates).sort();
Object.values(typedDataPoints).forEach(points => points.sort((a, b) => a.date.localeCompare(b.date)));
// Interpolate missing values
const interpolateValue = (date: string, points: { date: string, value: number }[]) => {
const index = points.findIndex(p => p.date >= date);
if (index === -1) return points[points.length - 1]?.value || 0;
if (index === 0) return points[0].value;
const prev = points[index - 1];
const next = points[index];
const totalDays = (new Date(next.date).getTime() - new Date(prev.date).getTime()) / (1000 * 60 * 60 * 24);
const daysSincePrev = (new Date(date).getTime() - new Date(prev.date).getTime()) / (1000 * 60 * 60 * 24);
return Number((prev.value + (next.value - prev.value) * (daysSincePrev / totalDays)).toFixed(3));
};
// Fill in all data points
sortedDates.forEach(date => {
const point: Partial<ChartDataPoint> = { date };
if (filters.showElectricity) point.electricity = interpolateValue(date, typedDataPoints.electricity);
if (filters.showGas) point.gas = interpolateValue(date, typedDataPoints.gas);
if (filters.showWaste) point.waste = interpolateValue(date, typedDataPoints.waste);
dataMap.set(date, point);
});
// Modify the return statement to truncate values
return Array.from(dataMap.values())
.filter(point => {
const date = new Date(point.date || '');
return (!filters.startDate || date >= filters.startDate) &&
(!filters.endDate || date <= filters.endDate);
})
.map(point => ({
...point,
electricity: point.electricity ? Number(point.electricity.toFixed(3)) : undefined,
gas: point.gas ? Number(point.gas.toFixed(3)) : undefined,
waste: point.waste ? Number(point.waste.toFixed(3)) : undefined,
}));
}, [building, filters]);
const pieChartData = useMemo(() => {
if (!building || !filters.showWaste) return [];
const wasteTypes = new Map<string, number>();
building.wasteGeneration.forEach((point: WasteDataPoint) => {
const type = point.wasteCategory.toLowerCase();
wasteTypes.set(type, (wasteTypes.get(type) || 0) + point.emissions);
});
return Array.from(wasteTypes, ([name, value]) => ({ name, value: Number(value.toFixed(3)) }));
}, [building, filters.showWaste]);
if (isLoading) {
return (
<div className="w-full h-96 bg-gray-200 animate-pulse rounded-lg">
{/* Skeleton content */}
<div className="h-full flex items-center justify-center">
<span className="text-gray-400">Loading...</span>
</div>
</div>
);
}
if (error) return <div>Error: {error.message}</div>;
const renderLineChart = () => (
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis label={{ value: 'Emissions (metric tons CO2e)', angle: -90, position: 'insideLeft', dy: 96 }} />
<Tooltip formatter={(value) => Number(value).toFixed(3)} />
<Legend />
{filters.showElectricity && building && building.electricityUsage.length > 0 &&
<Line type="monotone" dataKey="electricity" stroke="#8884d8" name="Electricity" connectNulls />}
{filters.showGas && building && building.naturalGasUsage.length > 0 &&
<Line type="monotone" dataKey="gas" stroke="#82ca9d" name="Natural Gas" connectNulls />}
{filters.showWaste && building && building.wasteGeneration.length > 0 &&
<Line type="monotone" dataKey="waste" stroke="#ffc658" name="Waste" connectNulls />}
</LineChart>
);
const renderAreaChart = () => (
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis label={{ value: 'Emissions (metric tons CO2e)', angle: -90, position: 'insideLeft' }} />
<Tooltip formatter={(value) => Number(value).toFixed(3)} />
<Legend />
{filters.showElectricity && building && building.electricityUsage.length > 0 &&
<Area type="monotone" dataKey="electricity" stackId="1" stroke="#8884d8" fill="#8884d8" name="Electricity" connectNulls />}
{filters.showGas && building && building.naturalGasUsage.length > 0 &&
<Area type="monotone" dataKey="gas" stackId="1" stroke="#82ca9d" fill="#82ca9d" name="Natural Gas" connectNulls />}
{filters.showWaste && building && building.wasteGeneration.length > 0 &&
<Area type="monotone" dataKey="waste" stackId="1" stroke="#ffc658" fill="#ffc658" name="Waste" connectNulls />}
</AreaChart>
);
const renderPieChart = () => (
<PieChart>
<Pie
data={pieChartData}
cx="50%"
cy="50%"
labelLine={false}
outerRadius={80}
fill="#8884d8"
dataKey="value"
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{pieChartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
);
return (
<div className="w-full h-96">
<ResponsiveContainer width="100%" height="100%">
{(() => {
switch (graphType) {
case 'line':
return renderLineChart();
case 'area':
return renderAreaChart();
case 'pie':
return renderPieChart();
default:
return <></>;
}
})()}
</ResponsiveContainer>
</div>
);
}

50
Project/components/face.tsx Executable file
View File

@@ -0,0 +1,50 @@
import React, { useEffect, useState } from "react";
import { motion } from "framer-motion";
interface FaceProps {
bin: string;
isVisible: boolean;
itemPosition: { x: number; y: number } | null;
videoDimensions: { width: number; height: number };
facePosition: { x: number; y: number };
}
const Face: React.FC<FaceProps> = ({ bin, isVisible, itemPosition, videoDimensions, facePosition }) => {
// Calculate eye rotation based on item position
const [eyeRotation, setEyeRotation] = useState<number>(0);
useEffect(() => {
if (itemPosition && isVisible) {
const dx = itemPosition.x - facePosition.x;
const dy = itemPosition.y - facePosition.y;
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
setEyeRotation(angle);
}
}, [itemPosition, isVisible, facePosition]);
return (
<motion.div
animate={{ y: isVisible ? 0 : 100 }} // Animate up when visible
className="face-container"
initial={{ y: 100 }} // Start below the screen
transition={{ duration: 0.5 }}
>
<div className="face">
{/* Face SVG or Graphics */}
<div className="eyes">
<div
className="eye left-eye"
style={{ transform: `rotate(${eyeRotation}deg)` }}
/>
<div
className="eye right-eye"
style={{ transform: `rotate(${eyeRotation}deg)` }}
/>
</div>
</div>
</motion.div>
);
};
export default Face;

230
Project/components/icons.tsx Executable file
View File

@@ -0,0 +1,230 @@
import * as React from "react";
import { IconSvgProps } from "@/types";
export const GithubIcon: React.FC<IconSvgProps> = ({
size = 24,
width,
height,
...props
}) => {
return (
<svg
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
clipRule="evenodd"
d="M12.026 2c-5.509 0-9.974 4.465-9.974 9.974 0 4.406 2.857 8.145 6.821 9.465.499.09.679-.217.679-.481 0-.237-.008-.865-.011-1.696-2.775.602-3.361-1.338-3.361-1.338-.452-1.152-1.107-1.459-1.107-1.459-.905-.619.069-.605.069-.605 1.002.07 1.527 1.028 1.527 1.028.89 1.524 2.336 1.084 2.902.829.091-.645.351-1.085.635-1.334-2.214-.251-4.542-1.107-4.542-4.93 0-1.087.389-1.979 1.024-2.675-.101-.253-.446-1.268.099-2.64 0 0 .837-.269 2.742 1.021a9.582 9.582 0 0 1 2.496-.336 9.554 9.554 0 0 1 2.496.336c1.906-1.291 2.742-1.021 2.742-1.021.545 1.372.203 2.387.099 2.64.64.696 1.024 1.587 1.024 2.675 0 3.833-2.33 4.675-4.552 4.922.355.308.675.916.675 1.846 0 1.334-.012 2.41-.012 2.737 0 .267.178.577.687.479C19.146 20.115 22 16.379 22 11.974 22 6.465 17.535 2 12.026 2z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
};
export const MoonFilledIcon = ({
size = 24,
width,
height,
...props
}: IconSvgProps) => (
<svg
aria-hidden="true"
focusable="false"
height={size || height}
role="presentation"
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
d="M21.53 15.93c-.16-.27-.61-.69-1.73-.49a8.46 8.46 0 01-1.88.13 8.409 8.409 0 01-5.91-2.82 8.068 8.068 0 01-1.44-8.66c.44-1.01.13-1.54-.09-1.76s-.77-.55-1.83-.11a10.318 10.318 0 00-6.32 10.21 10.475 10.475 0 007.04 8.99 10 10 0 002.89.55c.16.01.32.02.48.02a10.5 10.5 0 008.47-4.27c.67-.93.49-1.519.32-1.79z"
fill="currentColor"
/>
</svg>
);
export const SunFilledIcon = ({
size = 24,
width,
height,
...props
}: IconSvgProps) => (
<svg
aria-hidden="true"
focusable="false"
height={size || height}
role="presentation"
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<g fill="currentColor">
<path d="M19 12a7 7 0 11-7-7 7 7 0 017 7z" />
<path d="M12 22.96a.969.969 0 01-1-.96v-.08a1 1 0 012 0 1.038 1.038 0 01-1 1.04zm7.14-2.82a1.024 1.024 0 01-.71-.29l-.13-.13a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.984.984 0 01-.7.29zm-14.28 0a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a1 1 0 01-.7.29zM22 13h-.08a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zM2.08 13H2a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zm16.93-7.01a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a.984.984 0 01-.7.29zm-14.02 0a1.024 1.024 0 01-.71-.29l-.13-.14a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.97.97 0 01-.7.3zM12 3.04a.969.969 0 01-1-.96V2a1 1 0 012 0 1.038 1.038 0 01-1 1.04z" />
</g>
</svg>
);
export const HeartFilledIcon = ({
size = 24,
width,
height,
...props
}: IconSvgProps) => (
<svg
aria-hidden="true"
focusable="false"
height={size || height}
role="presentation"
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
d="M12.62 20.81c-.34.12-.9.12-1.24 0C8.48 19.82 2 15.69 2 8.69 2 5.6 4.49 3.1 7.56 3.1c1.82 0 3.43.88 4.44 2.24a5.53 5.53 0 0 1 4.44-2.24C19.51 3.1 22 5.6 22 8.69c0 7-6.48 11.13-9.38 12.12Z"
fill="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
/>
</svg>
);
export const SearchIcon = (props: IconSvgProps) => (
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="1em"
role="presentation"
viewBox="0 0 24 24"
width="1em"
{...props}
>
<path
d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
<path
d="M22 22L20 20"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
);
export const LoadingCircleIcon = ({
size = 24,
className,
...props
}: IconSvgProps & { size?: number; className?: string }) => (
<svg
className={`animate-spin text-current ${className}`}
fill="none"
height={size}
viewBox="0 0 24 24"
width={size}
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
/>
</svg>
);
export const LeftArrowIcon = ({
size = 24,
width,
height,
...props
}: IconSvgProps) => (
<svg
height={size || height}
viewBox="0 0 24 24"
width={size || width}
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M19 12H5M12 19L5 12L12 5"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
);
export const SettingsIcon = ({
size = 24,
width,
height,
...props
}: IconSvgProps) => (
<svg
fill="currentColor"
height={size || height}
viewBox="0 0 1024 1024"
width={size || width}
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M861.227 511.915a284.16 284.16 0 0 0-2.304-36.95 40.79 40.79 0 0 1 19.072-38.4l47.488-26.752a25.515 25.515 0 0 0 9.685-35.242l-90.07-152.406a26.837 26.837 0 0 0-36.095-9.429l-47.446 26.795a43.384 43.384 0 0 1-43.648-2.987 313.856 313.856 0 0 0-65.109-36.693 41.515 41.515 0 0 1-24.405-35.414v-53.333a26.155 26.155 0 0 0-26.368-25.77H421.845a26.155 26.155 0 0 0-26.325 25.727v53.632a41.515 41.515 0 0 1-24.448 35.584 315.35 315.35 0 0 0-65.067 36.566 43.264 43.264 0 0 1-43.69 2.986l-47.446-26.752a26.795 26.795 0 0 0-36.01 9.472L88.832 374.912a25.557 25.557 0 0 0 9.643 35.243l47.402 26.709c13.142 8.15 20.566 23.04 19.115 38.4a295.424 295.424 0 0 0 0 73.515 40.79 40.79 0 0 1-19.03 38.4l-47.487 26.709a25.515 25.515 0 0 0-9.643 35.2l90.027 152.448a26.88 26.88 0 0 0 36.053 9.515l47.403-26.795c14.037-6.997 30.72-5.888 43.648 2.944a315.596 315.596 0 0 0 65.066 36.523 41.515 41.515 0 0 1 24.491 35.584v53.546a26.197 26.197 0 0 0 26.283 25.814h180.181a26.155 26.155 0 0 0 26.368-25.728v-53.846a41.472 41.472 0 0 1 24.32-35.498 312.15 312.15 0 0 0 65.067-36.608A43.264 43.264 0 0 1 761.472 784l47.488 26.88a26.795 26.795 0 0 0 36.053-9.515l89.942-152.405a25.515 25.515 0 0 0-9.6-35.243l-47.446-26.752a40.79 40.79 0 0 1-19.072-38.4c1.622-12.117 2.39-24.405 2.39-36.693v.043zM511.7 654.25a142.25 142.25 0 1 1 .598-284.459 142.25 142.25 0 0 1-.598 284.459z"
fill="currentColor"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="0"
/>
</svg>
);
export const PlusIcon = ({
size = 24,
width,
height,
...props
}: IconSvgProps) => (
<svg
height={size || height}
viewBox="0 0 24 24"
width={size || width}
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M12 5V19M5 12H19"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
);

80
Project/components/navbar.tsx Executable file
View File

@@ -0,0 +1,80 @@
// components/navbar.tsx
"use client";
import {
Navbar as NextUINavbar,
NavbarContent,
NavbarBrand,
NavbarItem,
} from "@nextui-org/navbar";
import { Button } from "@nextui-org/button";
import { Link } from "@nextui-org/link";
import NextLink from "next/link";
import { usePathname } from 'next/navigation';
import Image from "next/image";
import { siteConfig } from "@/config/site";
import { ThemeSwitch } from "@/components/theme-switch";
import { GithubIcon } from "@/components/icons";
export const Navbar = () => {
const pathname = usePathname();
if (pathname.includes("/buildings/")) {
return <></>;
}
return (
<NextUINavbar maxWidth="xl" position="sticky">
<NavbarContent className="basis-1/5 sm:basis-full" justify="start">
<NavbarBrand as="li" className="gap-3">
<NextLink className="flex justify-start items-center" href="/">
<Image
alt="Carbin Logo"
className="h-8 w-8"
height={48}
src="/carbin.png"
width={48}
/>
<p className="text-2xl font-bold font-baskerville">Carbin</p>
</NextLink>
</NavbarBrand>
</NavbarContent>
<NavbarContent className="hidden sm:flex basis-1/5 sm:basis-full" justify="center">
<NavbarItem>
<Button
as={NextLink}
href="/buildings"
className="bg-orange-500 text-white min-w-[120px]"
>
Buildings
</Button>
</NavbarItem>
<NavbarItem>
<Button
as={NextLink}
href="/mission"
className="text-orange-500 min-w-[120px]"
variant="ghost"
>
Our Mission
</Button>
</NavbarItem>
</NavbarContent>
<NavbarContent
className="hidden sm:flex basis-1/5 sm:basis-full"
justify="end"
>
<NavbarItem className="hidden sm:flex gap-2">
<Link isExternal aria-label="Github" href={siteConfig.links.github}>
<GithubIcon className="text-default-500" />
</Link>
<ThemeSwitch />
</NavbarItem>
</NavbarContent>
</NextUINavbar>
);
};

View File

@@ -0,0 +1,53 @@
import { tv } from "tailwind-variants";
export const title = tv({
base: "tracking-tight inline font-semibold",
variants: {
color: {
violet: "from-[#FF1CF7] to-[#b249f8]",
yellow: "from-[#FF705B] to-[#FFB457]",
blue: "from-[#5EA2EF] to-[#0072F5]",
cyan: "from-[#00b7fa] to-[#01cfea]",
green: "from-[#6FEE8D] to-[#17c964]",
pink: "from-[#FF72E1] to-[#F54C7A]",
foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]",
},
size: {
sm: "text-3xl lg:text-4xl",
md: "text-[2.3rem] lg:text-5xl leading-9",
lg: "text-4xl lg:text-6xl",
},
fullWidth: {
true: "w-full block",
},
},
defaultVariants: {
size: "md",
},
compoundVariants: [
{
color: [
"violet",
"yellow",
"blue",
"cyan",
"green",
"pink",
"foreground",
],
class: "bg-clip-text text-transparent bg-gradient-to-b",
},
],
});
export const subtitle = tv({
base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full",
variants: {
fullWidth: {
true: "!w-full",
},
},
defaultVariants: {
fullWidth: true,
},
});

102
Project/components/sidebar.tsx Executable file
View File

@@ -0,0 +1,102 @@
// app/buildings/[buildingid]/sidebar/sidebar.tsx
"use client";
import { Link } from "@nextui-org/link";
import { Avatar } from "@nextui-org/avatar";
import { useState } from "react";
import { Button, Skeleton } from "@nextui-org/react";
import { usePathname } from "next/navigation";
import { ThemeSwitch } from "./theme-switch";
import { useBuilding } from "@/lib/useBuildingData";
import { GithubIcon, LeftArrowIcon } from "@/components/icons";
import { siteConfig } from "@/config/site";
interface SidebarProps {
buildingid: string;
}
export default function Sidebar({ buildingid }: SidebarProps) {
const { data: buildingData, error, isLoading } = useBuilding(buildingid);
const [isExpanded, setIsExpanded] = useState(true);
const pathname = usePathname();
if (pathname.includes("trashcan-mode")) {
return <></>;
}
return (
<div className={`flex flex-col items-center p-4 space-y-4 h-full ${isExpanded ? "w-64" : "w-16"}`}>
{/* Top section with info about building */}
<div className="flex flex-col items-center space-y-4 min-h-64 max-h-64">
{/* Back to all buildings */}
<Link href="/buildings">
<Button startContent={<LeftArrowIcon size={16} />} variant="light">
{"Back to all buildings"}
</Button>
</Link>
{/* Photo of building */}
{isLoading ? (
<Skeleton className="w-24 h-24 rounded-full">
<div className="w-24 h-24 rounded-full bg-default-300" />
</Skeleton>
) : error ? (
<div>Error: {error.message}</div>
) : !buildingData ? (
<div>No building found</div>
) : (
<Avatar
alt={buildingData.name}
className="w-24 h-24"
src={buildingData.imageURL}
/>
)}
{/* Name of building and settings button*/}
{isLoading ? (
<Skeleton className="w-40 h-8 mb-4">
<div className="w-40 h-8 bg-default-300" />
</Skeleton>
) : buildingData ? (
<div className="flex flex-row items-center justify-between">
<h2 className="text-xl font-bold mb-4">{buildingData.name}</h2>
</div>
) : null}
</div>
{/* Middle section with navigation links */}
<nav className="flex flex-col space-y-6 h-full">
<Link color="primary" href={`/buildings/${buildingid}/emissions`}>
{pathname === `/buildings/${buildingid}/emissions` ? <strong>Emissions</strong> : "Emissions"}
</Link>
<Link color="primary" href={`/buildings/${buildingid}/trash`}>
{pathname === `/buildings/${buildingid}/trash` ? <strong>Trash Log</strong> : "Trash Log"}
</Link>
<Link color="primary" href={`/buildings/${buildingid}/trash-scanner`}>
{pathname === `/buildings/${buildingid}/trash-scanner` ? <strong>Trash Scanner</strong> : "Trash Scanner"}
</Link>
<Link color="primary" href={`/buildings/${buildingid}/trashcan-mode`}>
{pathname === `/buildings/${buildingid}/trashcan-mode` ? <strong>Trashcan Mode</strong> : "Trashcan Mode"}
</Link>
</nav>
{/* Bottom section with quick actions */}
<div className="flex items-center space-x-2 bg-default-100 rounded-full p-2">
<ThemeSwitch />
<div className="w-px h-6 bg-divider" /> {/* Vertical divider */}
<Link isExternal aria-label="Github" className="p-0" href={siteConfig.links.github}>
<GithubIcon className="text-default-500" />
</Link>
{/* <div className="w-px h-6 bg-divider" />
<Link aria-label="Settings" className="p-0" href={"/settings"}>
<SettingsIcon className="text-default-500" />
</Link> */}
</div>
</div>
);
}

View File

@@ -0,0 +1,81 @@
"use client";
import { FC } from "react";
import { VisuallyHidden } from "@react-aria/visually-hidden";
import { SwitchProps, useSwitch } from "@nextui-org/switch";
import { useTheme } from "next-themes";
import { useIsSSR } from "@react-aria/ssr";
import clsx from "clsx";
import { SunFilledIcon, MoonFilledIcon } from "@/components/icons";
export interface ThemeSwitchProps {
className?: string;
classNames?: SwitchProps["classNames"];
}
export const ThemeSwitch: FC<ThemeSwitchProps> = ({
className,
classNames,
}) => {
const { theme, setTheme } = useTheme();
const isSSR = useIsSSR();
const onChange = () => {
theme === "light" ? setTheme("dark") : setTheme("light");
};
const {
Component,
slots,
isSelected,
getBaseProps,
getInputProps,
getWrapperProps,
} = useSwitch({
isSelected: theme === "light" || isSSR,
"aria-label": `Switch to ${theme === "light" || isSSR ? "dark" : "light"} mode`,
onChange,
});
return (
<Component
{...getBaseProps({
className: clsx(
"px-px transition-opacity hover:opacity-80 cursor-pointer",
className,
classNames?.base,
),
})}
>
<VisuallyHidden>
<input {...getInputProps()} />
</VisuallyHidden>
<div
{...getWrapperProps()}
className={slots.wrapper({
class: clsx(
[
"w-auto h-auto",
"bg-transparent",
"rounded-lg",
"flex items-center justify-center",
"group-data-[selected=true]:bg-transparent",
"!text-default-500",
"pt-px",
"px-0",
"mx-0",
],
classNames?.wrapper,
),
})}
>
{!isSelected || isSSR ? (
<SunFilledIcon size={22} />
) : (
<MoonFilledIcon size={22} />
)}
</div>
</Component>
);
};

View File

@@ -0,0 +1,175 @@
// trashDetection.tsx
/* eslint-disable no-console */
"use client";
import React, { useEffect, useRef, useState, useMemo } from "react";
import { InferenceEngine, CVImage } from "inferencejs";
function RealtimeModel() {
const inferEngine = useMemo(() => new InferenceEngine(), []);
const [modelWorkerId, setModelWorkerId] = useState<string | null>(null);
const modelWorkerIdRef = useRef<string | null>(null);
const [modelLoading, setModelLoading] = useState(false);
const [predictions, setPredictions] = useState<any[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
// References to manage media stream and timeouts
const mediaStreamRef = useRef<MediaStream | null>(null);
const detectFrameTimeoutRef = useRef<number | null>(null);
useEffect(() => {
console.log("Component mounted");
setModelLoading(true);
inferEngine
.startWorker("trash-detection-kkthk", 7, "rf_1nBQDUSClLUApDgPjG78qMbBH602")
.then((id) => {
setModelWorkerId(id);
modelWorkerIdRef.current = id;
startWebcam();
})
.catch((error) => {
console.error("Error starting model worker:", error);
});
// Cleanup function to stop the model worker and webcam when the component unmounts
return () => {
console.log("Component unmounting, stopping model worker and webcam");
if (modelWorkerIdRef.current) {
inferEngine.stopWorker(modelWorkerIdRef.current);
console.log(`Stopped model worker with ID: ${modelWorkerIdRef.current}`);
}
stopWebcam();
if (detectFrameTimeoutRef.current) {
clearTimeout(detectFrameTimeoutRef.current);
detectFrameTimeoutRef.current = null;
console.log("Cleared detectFrameTimeoutRef");
}
};
}, [inferEngine]);
const startWebcam = () => {
const constraints = {
audio: false,
video: {
facingMode: "environment",
},
};
navigator.mediaDevices
.getUserMedia(constraints)
.then((stream) => {
mediaStreamRef.current = stream; // Store the stream reference
if (videoRef.current && containerRef.current) {
videoRef.current.srcObject = stream;
videoRef.current.onloadedmetadata = () => {
videoRef.current?.play();
};
videoRef.current.onplay = () => {
if (canvasRef.current && videoRef.current && containerRef.current) {
detectFrame();
}
};
}
})
.catch((error) => {
console.error("Error accessing webcam:", error);
});
};
const stopWebcam = () => {
if (mediaStreamRef.current) {
console.log("Stopping webcam...");
mediaStreamRef.current.getTracks().forEach((track) => {
track.stop();
console.log(`Stopped track: ${track.kind}`);
});
mediaStreamRef.current = null;
} else {
console.log("No media stream to stop.");
}
if (videoRef.current) {
videoRef.current.pause();
videoRef.current.srcObject = null;
console.log("Video paused and srcObject cleared.");
}
};
const detectFrame = () => {
if (!modelWorkerIdRef.current) {
detectFrameTimeoutRef.current = window.setTimeout(detectFrame, 1000 / 3);
return;
}
if (videoRef.current && canvasRef.current) {
const img = new CVImage(videoRef.current);
inferEngine.infer(modelWorkerIdRef.current, img).then((newPredictions) => {
const ctx = canvasRef.current!.getContext("2d")!;
// Clear the canvas
ctx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height);
// Get the scaling factors
const scaleX = canvasRef.current!.width / (videoRef.current!.videoWidth ?? 1);
const scaleY = canvasRef.current!.height / (videoRef.current!.videoHeight ?? 1);
newPredictions.forEach((prediction: any) => {
const x = (prediction.bbox.x - prediction.bbox.width / 2) * scaleX;
const y = (prediction.bbox.y - prediction.bbox.height / 2) * scaleY;
const width = prediction.bbox.width * scaleX;
const height = prediction.bbox.height * scaleY;
// Draw bounding box
ctx.strokeStyle = prediction.color;
ctx.lineWidth = 1;
ctx.strokeRect(x, y, width, height);
});
setPredictions(newPredictions);
detectFrameTimeoutRef.current = window.setTimeout(detectFrame, 1000 / 3);
}).catch((error) => {
console.error("Error during inference:", error);
});
}
};
return (
<div ref={containerRef} className="w-full h-screen relative overflow-hidden">
<div className="absolute inset-0">
<video
ref={videoRef}
muted
playsInline
className="absolute top-0 left-0 w-full h-full transform scale-x-[-1]"
>
<track kind="captions" />
</video>
<canvas
ref={canvasRef}
className="absolute top-0 left-0 w-full h-full transform scale-x-[-1]"
/>
</div>
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 w-11/12 max-w-2xl">
<div className="bg-black bg-opacity-50 backdrop-filter backdrop-blur-md rounded-xl p-4 text-white text-center">
{predictions.length > 0 ? (
predictions.map((prediction, index) => (
<div key={index} className="text-lg">
{`${prediction.class} - ${Math.round(prediction.confidence * 100)}%`}
</div>
))
) : (
<div className="text-lg">No Item Detected</div>
)}
</div>
</div>
</div>
);
}
export default RealtimeModel;

View File

@@ -0,0 +1,681 @@
/* eslint-disable no-console */
"use client";
import React, { useEffect, useRef, useState, useMemo } from "react";
import { InferenceEngine, CVImage } from "inferencejs";
import { motion } from "framer-motion";
import { Timestamp } from 'firebase/firestore';
import { useParams } from "next/navigation";
import { useBuilding, WasteDataPoint } from '@/lib/useBuildingData';
import { Card } from "@nextui-org/react";
export const trashItems = [
{
id: "Aluminum-Can",
name: "Aluminum Can",
bin: "Recycling",
co2e: 170,
},
{
id: "Aluminum-Foil",
name: "Aluminum Foil",
bin: "Recycling",
note: "Please rinse and flatten",
co2e: 10,
},
{
id: "Bio-Plastic-Cup",
name: "Bio-Plastic Cup",
bin: "Compost",
co2e: 70,
},
{
id: "Cardboard",
name: "Cardboard",
bin: "Recycling",
note: "Please flatten all cardboard",
co2e: 80,
},
{
id: "Food",
name: "Food",
bin: "Compost",
co2e: 1000,
},
{
id: "Food-Wrapper",
name: "Food Wrapper",
bin: "Landfill",
co2e: 6,
},
{
id: "Paper",
name: "Paper",
bin: "Recycling",
co2e: 8,
},
{
id: "Paper-Cup",
name: "Paper Cup",
bin: "Recycling",
co2e: 11,
},
{
id: "Paper-Plate",
name: "Paper Plate",
bin: "Compost",
co2e: 15,
},
{
id: "Paper-Soft",
name: "Soft Paper",
bin: "Recycling",
co2e: 5,
},
{
id: "Plastic-Bag",
name: "Plastic Bag",
bin: "Landfill",
co2e: 33,
},
{
id: "Plastic-Bottle",
name: "Plastic Bottle",
bin: "Recycling",
note: "Only hard number 1 or 2 bottles",
co2e: 82,
},
{
id: "Plastic-Container",
name: "Plastic Container",
bin: "Recycling",
note: "Only hard plastics number 1 or 2",
co2e: 100,
},
{
id: "Plastic-Cup",
name: "Plastic Cup",
bin: "Recycling",
note: "Only hard plastics number 1 or 2",
co2e: 30,
},
{
id: "Plastic-Utensil",
name: "Plastic Utensil",
bin: "Landfill",
co2e: 8,
},
{
id: "Styrofoam",
name: "Styrofoam",
bin: "Landfill",
co2e: 45,
},
];
interface BBox {
x: number;
y: number;
width: number;
height: number;
}
interface Prediction {
class: string;
confidence: number;
bbox: BBox;
color: string;
}
interface Detection {
className: string;
lastSeen: number;
framesSeen: number;
bbox: BBox;
isActive: boolean;
}
function TrashcanMode() {
// Initialize the inference engine and state variables
const inferEngine = useMemo(() => new InferenceEngine(), []);
const [modelWorkerId, setModelWorkerId] = useState<string | null>(null);
const [modelLoading, setModelLoading] = useState(false);
const [currentItem, setCurrentItem] = useState<any | null>(null); // Current detected item
const [thrownItems, setThrownItems] = useState<string[]>([]); // List of items estimated to be thrown away
const [showCelebration, setShowCelebration] = useState(false); // State to trigger celebration
const [showCamera, setShowCamera] = useState(false); // Default to false as per your preference
const [isHovering, setIsHovering] = useState(false); // State to detect hover over the switch area
// state variables for ripple effect
const [rippleActive, setRippleActive] = useState(false);
const [rippleColor, setRippleColor] = useState<string>('');
const [ripplePosition, setRipplePosition] = useState<{ x: string; y: string }>({ x: '50%', y: '50%' });
// References to DOM elements
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
// Tracking detections over time
const detectionsRef = useRef<{ [className: string]: Detection }>({}); // Ref to store detection history
// Introduce a ref to keep track of the last active item and its timestamp
const lastActiveItemRef = useRef<{ itemDetails: any | null; timestamp: number }>({
itemDetails: null,
timestamp: 0,
});
// Inside the component, get the building data
const { buildingid } = useParams();
const { data: building, isLoading, error, updateBuilding } = useBuilding(buildingid as string);
// Helper function to get bin emoji
const getBinEmoji = (bin: string) => {
switch (bin) {
case "Recycling":
return "♻️";
case "Compost":
return "🌿";
case "Landfill":
return "🗑️";
default:
return "";
}
};
// Helper function to get item emoji
const getItemEmoji = (itemId: string) => {
switch (itemId) {
case "Aluminum-Can":
return "🥫";
case "Aluminum-Foil":
return "🥄";
case "Bio-Plastic-Cup":
return "🥤";
case "Cardboard":
return "📦";
case "Food":
return "🍎";
case "Food-Wrapper":
return "🍬";
case "Paper":
return "📄";
case "Paper-Cup":
return "☕";
case "Paper-Plate":
return "🍽️";
case "Paper-Soft":
return "📃";
case "Plastic-Bag":
return "🛍️";
case "Plastic-Bottle":
return "🍼";
case "Plastic-Container":
return "🍱";
case "Plastic-Cup":
return "🥛";
case "Plastic-Utensil":
return "🍴";
case "Styrofoam":
return "📦";
default:
return "";
}
};
// helper function for ripple start position
const getBinRippleStartPosition = (bin: string) => {
switch (bin) {
case "Recycling":
return { x: '100%', y: '100%' }; // Bottom-right corner
case "Compost":
return { x: '50%', y: '100%' }; // Bottom-center
case "Landfill":
return { x: '0%', y: '100%' }; // Bottom-left corner
default:
return { x: '50%', y: '50%' }; // Center
}
};
// Effect to start the model worker
useEffect(() => {
if (!modelLoading) {
setModelLoading(true);
inferEngine
.startWorker("trash-detection-kkthk", 7, "rf_1nBQDUSClLUApDgPjG78qMbBH602")
.then((id) => setModelWorkerId(id))
.catch((error) => {
console.error("Error starting model worker:", error);
});
}
}, [inferEngine, modelLoading]);
// Effect to start the webcam when the model worker is ready
useEffect(() => {
if (modelWorkerId) {
startWebcam();
}
}, [modelWorkerId]);
// Function to initialize and start the webcam
const startWebcam = () => {
const constraints = {
audio: false,
video: {
width: { ideal: 640 },
height: { ideal: 480 },
facingMode: "environment",
},
};
navigator.mediaDevices
.getUserMedia(constraints)
.then((stream) => {
if (videoRef.current) {
videoRef.current.srcObject = stream;
videoRef.current.onloadedmetadata = () => {
videoRef.current?.play();
};
videoRef.current.onplay = () => {
detectFrame();
};
}
})
.catch((error) => {
console.error("Error accessing webcam:", error);
});
};
// Function to detect objects in each video frame
const detectFrame = () => {
if (!modelWorkerId || !videoRef.current) {
setTimeout(detectFrame, 1000 / 3);
return;
}
const img = new CVImage(videoRef.current);
inferEngine.infer(modelWorkerId, img).then((predictions: unknown) => {
const typedPredictions = predictions as Prediction[];
const videoWidth = videoRef.current?.videoWidth ?? 640;
const videoHeight = videoRef.current?.videoHeight ?? 480;
const now = Date.now();
// Filter predictions above confidence threshold
const validPredictions = typedPredictions.filter((pred) => pred.confidence >= 0.2);
if (showCamera && canvasRef.current) {
const ctx = canvasRef.current.getContext("2d")!;
const canvasWidth = canvasRef.current.width;
const canvasHeight = canvasRef.current.height;
// Clear the canvas
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// Draw trash can regions
drawTrashcanRegions(ctx, videoWidth, videoHeight, canvasWidth, canvasHeight);
// Get scaling factors
const scaleX = canvasWidth / (videoWidth ?? 1);
const scaleY = canvasHeight / (videoHeight ?? 1);
validPredictions.forEach((pred: Prediction) => {
// Draw bounding box and center point
drawBoundingBox(ctx, pred, scaleX, scaleY);
});
}
validPredictions.forEach((pred: Prediction) => {
const className = pred.class;
const bbox = pred.bbox;
// Initialize tracking for this class if not present
if (!detectionsRef.current[className]) {
detectionsRef.current[className] = {
className: className,
lastSeen: now,
framesSeen: 1,
bbox: bbox,
isActive: false,
};
} else {
// Update tracking info
const detection = detectionsRef.current[className];
detection.lastSeen = now;
detection.framesSeen += 1;
detection.bbox = bbox;
// Mark as active if seen consistently over 3 frames
if (detection.framesSeen >= 3 && !detection.isActive) {
detection.isActive = true;
}
}
});
// Remove stale detections and check if any active detections are present
let activeDetections = Object.values(detectionsRef.current).filter((detection) => {
const timeSinceLastSeen = now - detection.lastSeen;
if (timeSinceLastSeen > 1000) {
// Remove stale detections
if (detection.isActive) {
// Determine if last known position was near the correct trashcan area
const itemDetails = trashItems.find((item) => item.id === detection.className);
if (itemDetails) {
const isNearCorrectTrashcan = checkIfNearTrashcanArea(
detection.bbox,
itemDetails.bin,
videoWidth,
videoHeight
);
if (isNearCorrectTrashcan) {
// Item was likely thrown away in the correct bin
setThrownItems((prevItems) => [...prevItems, detection.className]);
setShowCelebration(true); // Trigger celebration
setTimeout(() => setShowCelebration(false), 3000); // Stop celebration after 3 seconds
// Trigger the ripple effect
setRippleColor(getBinColor(itemDetails.bin));
setRipplePosition(getBinRippleStartPosition(itemDetails.bin));
setRippleActive(true);
setTimeout(() => setRippleActive(false), 3000); // Ripple lasts 3 seconds
const adjustedEmissions = itemDetails.co2e / 1e+3; // Convert kg to tons
const newWasteDataPoint: WasteDataPoint = {
timestamp: Timestamp.now(),
type: itemDetails.id,
trashcanID: '1', // Use trashcan ID 1
wasteCategory: itemDetails.bin,
emissions: adjustedEmissions,
};
// Update the building's waste generation data
const updatedWasteGeneration = [
...(building?.wasteGeneration || []),
newWasteDataPoint,
];
updateBuilding({ wasteGeneration: updatedWasteGeneration });
} else {
// Incorrect bin, do not trigger celebration
setCurrentItem(null);
}
}
}
delete detectionsRef.current[detection.className];
return false;
}
return detection.isActive;
});
// Update the current item for display based on active detections
if (activeDetections.length > 0) {
// Find the most recently seen active detection
activeDetections.sort((a, b) => b.lastSeen - a.lastSeen);
const mostRecentDetection = activeDetections[0];
const itemDetails = trashItems.find((item) => item.id === mostRecentDetection.className);
// Update last active item reference
lastActiveItemRef.current = { itemDetails, timestamp: now };
setCurrentItem(itemDetails);
} else {
// If no active detections, retain the last item for a short duration
if (now - lastActiveItemRef.current.timestamp < 1000) {
setCurrentItem(lastActiveItemRef.current.itemDetails);
} else {
setCurrentItem(null);
lastActiveItemRef.current = { itemDetails: null, timestamp: 0 };
}
}
setTimeout(detectFrame, 1000 / 3);
});
};
// Helper function to draw bounding box and center point
const drawBoundingBox = (ctx: CanvasRenderingContext2D, prediction: Prediction, scaleX: number, scaleY: number) => {
const x = (prediction.bbox.x - prediction.bbox.width / 2) * scaleX;
const y = (prediction.bbox.y - prediction.bbox.height / 2) * scaleY;
const width = prediction.bbox.width * scaleX;
const height = prediction.bbox.height * scaleY;
// Draw bounding box
ctx.strokeStyle = prediction.color || "#FF0000";
ctx.lineWidth = 2;
ctx.strokeRect(x, y, width, height);
// Draw center point
ctx.fillStyle = prediction.color || "#FF0000";
ctx.beginPath();
ctx.arc(x + width / 2, y + height / 2, 5, 0, 2 * Math.PI);
ctx.fill();
};
// Helper function to draw trashcan regions
const drawTrashcanRegions = (
ctx: CanvasRenderingContext2D,
videoWidth: number,
videoHeight: number,
canvasWidth: number,
canvasHeight: number
) => {
const trashcanAreas = getTrashcanAreas(videoWidth, videoHeight);
const scaleX = canvasWidth / (videoWidth ?? 1);
const scaleY = canvasHeight / (videoHeight ?? 1);
Object.entries(trashcanAreas).forEach(([bin, area]) => {
const x = area.x * scaleX;
const y = area.y * scaleY;
const width = area.width * scaleX;
const height = area.height * scaleY;
ctx.strokeStyle = getBinColor(bin);
ctx.lineWidth = 2;
ctx.strokeRect(x, y, width, height);
// Optionally, fill the area with transparent color
ctx.fillStyle = getBinColor(bin) + "33"; // Add transparency
ctx.fillRect(x, y, width, height);
});
};
// Helper function to check if the bounding box is near the correct trashcan area
const checkIfNearTrashcanArea = (
bbox: BBox,
correctBin: string,
videoWidth: number,
videoHeight: number
): boolean => {
const centerX = bbox.x;
const centerY = bbox.y;
// Define areas for each trashcan
const trashcanAreas = getTrashcanAreas(videoWidth, videoHeight);
// Check if the center point is within any trashcan area
for (const [bin, area] of Object.entries(trashcanAreas)) {
if (
centerX >= area.x &&
centerX <= area.x + area.width &&
centerY >= area.y &&
centerY <= area.y + area.height
) {
const isCorrect = bin === correctBin;
return isCorrect;
}
}
// If not near any bin
return false;
};
// Helper function to define trashcan areas
const getTrashcanAreas = (videoWidth: number, videoHeight: number) => {
const areaWidth = (videoWidth * 2) / 5; // 2/5 of the screen width
const areaHeight = videoHeight / 2; // 1/2 of the screen height
return {
Recycling: {
x: 0,
y: videoHeight / 2,
width: areaWidth,
height: areaHeight,
},
Compost: {
x: (videoWidth - areaWidth) / 2,
y: videoHeight / 2,
width: areaWidth,
height: areaHeight,
},
Landfill: {
x: videoWidth - areaWidth,
y: videoHeight / 2,
width: areaWidth,
height: areaHeight,
},
};
};
// Helper function to get bin color
const getBinColor = (bin: string) => {
switch (bin) {
case "Recycling":
return "#00aaff"; // Blue
case "Compost":
return "#33cc33"; // Green
case "Landfill":
return "#aaaaaa"; // Gray
default:
return "#ffffff"; // White
}
};
// Helper function to get arrow symbol
const getArrow = (bin: string) => {
switch (bin) {
case "Recycling":
return "→"; // Right arrow
case "Compost":
return "↓"; // Down arrow
case "Landfill":
return "←"; // Left arrow
default:
return "";
}
};
// Render the component
return (
<div
ref={containerRef}
className="w-full h-screen relative overflow-hidden bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900"
>
{/* Hidden video element for capturing webcam feed */}
<video ref={videoRef} muted playsInline className="hidden">
<track kind="captions" />
</video>
{/* Video and canvas elements for display */}
{showCamera && (
<div className="absolute inset-0">
<video
ref={videoRef}
muted
playsInline
className="absolute top-0 left-0 w-full h-full object-cover opacity-30"
style={{ transform: "scaleX(-1)" }}
>
<track kind="captions" />
</video>
<canvas
ref={canvasRef}
className="absolute top-0 left-0 w-full h-full object-cover pointer-events-none"
style={{ transform: "scaleX(-1)" }}
/>
</div>
)}
{/* Ripple Effect Overlay */}
{rippleActive && (
<div
className="ripple-effect"
style={{
backgroundColor: rippleColor,
left: ripplePosition.x,
top: ripplePosition.y,
}}
/>
)}
{/* Main content */}
<div className="relative z-10 flex flex-col justify-center items-center w-full h-full p-8">
{showCelebration ? (
<motion.div
className="flex flex-col items-center"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.5, type: "spring", stiffness: 100 }}
>
<span aria-label="Check Mark" className="text-9xl mb-4" role="img">
</span>
<h2 className="text-4xl font-bold text-green-600 dark:text-green-400">
Great job!
</h2>
</motion.div>
) : currentItem ? (
<Card className="w-full max-w-2xl p-8 bg-white dark:bg-gray-800 shadow-lg">
<h1 className="text-5xl font-bold text-center mb-8 text-gray-800 dark:text-gray-200">
{getBinEmoji(currentItem.bin)} {currentItem.bin} {getArrow(currentItem.bin)}
</h1>
<h2 className="text-3xl text-center mb-4 text-gray-700 dark:text-gray-300">
{getItemEmoji(currentItem.id)} {currentItem.name}
</h2>
{currentItem.note && (
<p className="text-xl text-center text-gray-600 dark:text-gray-400">
{currentItem.note}
</p>
)}
</Card>
) : (
<Card className="w-full max-w-2xl p-8 bg-white dark:bg-gray-800 shadow-lg">
<h1 className="text-4xl font-bold text-center mb-8 text-gray-800 dark:text-gray-200">
No Item Detected
</h1>
{thrownItems.length > 0 && (
<div className="text-center">
<h2 className="text-xl font-semibold mb-2 text-gray-700 dark:text-gray-300">
Recently Thrown Items:
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
{Object.entries(
thrownItems.slice(-5).reduce((acc, item) => {
acc[item] = (acc[item] || 0) + 1;
return acc;
}, {} as Record<string, number>)
)
.map(([item, count]) => (count > 1 ? `${item} (${count}x)` : item))
.join(", ")}
</p>
</div>
)}
</Card>
)}
</div>
</div>
);
}
export default TrashcanMode;

View File

@@ -0,0 +1,415 @@
// components/uploadDataModal.tsx
import { useState, useEffect } from "react";
import { Button, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@nextui-org/react";
import { Accordion, AccordionItem } from "@nextui-org/react";
import { AzureKeyCredential, DocumentAnalysisClient } from "@azure/ai-form-recognizer";
import { useRouter } from 'next/navigation';
import { ElectricityDataPoint, NaturalGasDataPoint } from "../lib/useBuildingData";
interface UploadDataModalProps {
isOpen: boolean;
onClose: () => void;
buildingid: string;
updateBuilding: (newData: any) => void;
}
const EMISSIONS_FACTOR = 0.5;
const key = process.env.NEXT_PUBLIC_FORM_RECOGNIZER_KEY;
const endpoint = process.env.NEXT_PUBLIC_FORM_RECOGNIZER_ENDPOINT;
export function UploadDataModal({ isOpen, onClose, buildingid, updateBuilding }: UploadDataModalProps) {
const [isSubmitted, setIsSubmitted] = useState(false);
const [gasFile, setGasFile] = useState<File | null>(null);
const [electricityFile, setElectricityFile] = useState<File | null>(null);
const [gasFileUrl, setGasFileUrl] = useState<string | null>(null);
const [electricityFileUrl, setElectricityFileUrl] = useState<string | null>(null);
const [extractionStatus, setExtractionStatus] = useState<'idle' | 'loading' | 'complete'>('idle');
const [aiExtractionStatus, setAiExtractionStatus] = useState<'idle' | 'loading' | 'complete'>('idle');
const [dataPreview, setDataPreview] = useState<any>(null);
const router = useRouter();
const handleFileUpload = (type: 'gas' | 'electricity', file: File) => {
if (type === 'gas') {
setGasFile(file);
setGasFileUrl(URL.createObjectURL(file));
} else if (type === 'electricity') {
setElectricityFile(file);
setElectricityFileUrl(URL.createObjectURL(file));
}
setIsSubmitted(true);
};
useEffect(() => {
return () => {
if (gasFileUrl) URL.revokeObjectURL(gasFileUrl);
if (electricityFileUrl) URL.revokeObjectURL(electricityFileUrl);
};
}, [gasFileUrl, electricityFileUrl]);
const extractDataFromPDF = async (file: File, type: 'gas' | 'electricity') => {
const client = new DocumentAnalysisClient(endpoint!, new AzureKeyCredential(key!));
const arrayBuffer = await file.arrayBuffer();
const poller = await client.beginAnalyzeDocument("prebuilt-document", arrayBuffer);
const { keyValuePairs } = await poller.pollUntilDone();
if (!keyValuePairs) return [];
const dataPoints: (ElectricityDataPoint | NaturalGasDataPoint)[] = [];
let extractedDate: Date | null = null;
const monthMap: { [key: string]: number } = {
'jan': 0, 'january': 0, 'feb': 1, 'february': 1, 'mar': 2, 'march': 2,
'apr': 3, 'april': 3, 'may': 4, 'jun': 5, 'june': 5, 'jul': 6, 'july': 6,
'aug': 7, 'august': 7, 'sep': 8, 'september': 8, 'oct': 9, 'october': 9,
'nov': 10, 'november': 10, 'dec': 11, 'december': 11
};
for (const { key, value } of keyValuePairs) {
console.log("KEY:", key.content, "VALUE:", value?.content);
if (!value) continue;
const keyLower = key.content.toLowerCase();
const valueLower = value.content.toLowerCase();
// Extract date information
if (keyLower.includes('date') || keyLower.includes('period')) {
console.log("DATE IDENTIFIED:", valueLower);
const dateMatch = valueLower.match(/(\d{1,2})\s*(?:st|nd|rd|th)?\s*(?:of)?\s*([a-z]+)?\s*(\d{4})?/i);
console.log("DATE MATCH:", dateMatch);
if (dateMatch) {
const day = 1; // Always assume 1st of the month
const month = dateMatch[2] ? monthMap[dateMatch[2].toLowerCase()] : new Date().getMonth();
const year = dateMatch[3] ? parseInt(dateMatch[3]) : new Date().getFullYear();
if (year >= 1900 && year <= 2100) {
extractedDate = new Date(year, month, day);
}
}
}
if (type === 'electricity' && keyLower.includes('kwh')) {
const kwh = parseFloat(value.content || '0');
if (kwh !== 0) {
const timestamp = extractedDate || new Date();
timestamp.setHours(0, 0, 0, 0); // Set to midnight
const existingDataIndex = dataPoints.findIndex(point =>
point.timestamp.seconds === timestamp.getTime() / 1000
);
if (existingDataIndex === -1) {
dataPoints.push({
timestamp: { seconds: timestamp.getTime() / 1000, nanoseconds: 0 },
kwh: kwh,
emissions: kwh * EMISSIONS_FACTOR / 1000,
});
} else {
dataPoints[existingDataIndex] = {
...dataPoints[existingDataIndex],
kwh: kwh,
emissions: kwh * EMISSIONS_FACTOR / 1000,
};
}
}
} else if (type === 'gas' && keyLower.includes('therm')) {
const therms = parseFloat(value.content || '0');
if (therms !== 0) {
const timestamp = extractedDate || new Date();
timestamp.setHours(0, 0, 0, 0); // Set to midnight
const existingDataIndex = dataPoints.findIndex(point =>
point.timestamp.seconds === timestamp.getTime() / 1000
);
if (existingDataIndex === -1) {
dataPoints.push({
timestamp: { seconds: timestamp.getTime() / 1000, nanoseconds: 0 },
therms: therms,
emissions: therms * 5.3 / 1000, // approx CO2 emissions for natural gas (5.3 kg CO2 per therm, measured in tons)
});
} else {
dataPoints[existingDataIndex] = {
...dataPoints[existingDataIndex],
therms: therms,
emissions: therms * 5.3 / 1000,
};
}
}
}
}
return dataPoints;
};
const handleExtraction = async () => {
setExtractionStatus('loading');
try {
let newData: any = {};
if (gasFile) {
const gasData = await extractDataFromPDF(gasFile, 'gas');
console.log("Gas data:");
gasData.forEach(dataPoint => {
console.log("Date:", new Date(dataPoint.timestamp.seconds * 1000).toLocaleDateString(), "Therms:", (dataPoint as NaturalGasDataPoint).therms);
});
newData.naturalGasUsage = gasData;
}
if (electricityFile) {
const electricityData = await extractDataFromPDF(electricityFile, 'electricity');
console.log("Electricity data:");
electricityData.forEach(dataPoint => {
console.log("Date:", new Date(dataPoint.timestamp.seconds * 1000).toLocaleDateString(), "kWh:", (dataPoint as ElectricityDataPoint).kwh);
});
newData.electricityUsage = electricityData;
}
setDataPreview(newData);
setExtractionStatus('complete');
// Update the building data
updateBuilding(newData);
} catch (error) {
console.error("Error during extraction:", error);
setExtractionStatus('idle');
}
};
const handleAIExtraction = async () => {
setAiExtractionStatus('loading');
try {
let newData: any = {};
if (gasFile) {
const gasData = await extractDataUsingAI(gasFile, 'gas');
newData.naturalGasUsage = gasData;
}
if (electricityFile) {
const electricityData = await extractDataUsingAI(electricityFile, 'electricity');
newData.electricityUsage = electricityData;
}
setDataPreview(newData);
setAiExtractionStatus('complete');
// Update the building data
updateBuilding(newData);
} catch (error) {
console.error("Error during AI extraction:", error);
setAiExtractionStatus('idle');
}
};
const extractDataUsingAI = async (file: File, type: 'gas' | 'electricity') => {
// Step 1: Convert PDF to image
const formData = new FormData();
formData.append('pdf', file, file.name);
formData.append('type', type);
const pdfToImageResponse = await fetch('/api/pdf-to-image', {
method: 'POST',
body: formData,
});
if (!pdfToImageResponse.ok) {
throw new Error('Failed to convert PDF to image');
}
const { response } = await pdfToImageResponse.json();
console.log("PDF TO IMAGE RESPONSE", response);
// Parse the JSON response
const parsedData: string = response.response;
//Trim the string to remove the "anything before first {" and "and after last }"
const trimmedData = parsedData.replace(/^[^{]*|[^}]*$/g, '');
const parsedTrimmedData = JSON.parse(trimmedData);
console.log("PARSED TRIMMED DATA", parsedTrimmedData);
// Convert the parsed data to the format expected by the application
return parsedTrimmedData.dataPoints.map((point: any) => ({
timestamp: {
seconds: new Date(point.date).getTime() / 1000,
nanoseconds: 0
},
[type === 'gas' ? 'therms' : 'kwh']: point.usage,
emissions: point.usage * (type === 'gas' ? 5.3 : EMISSIONS_FACTOR) / 1000,
}));
};
return (
<Modal backdrop="blur" isOpen={isOpen} size="xl" onClose={onClose}>
<ModalContent>
{!isSubmitted ? (
<>
<ModalHeader>Upload New Data</ModalHeader>
<ModalBody>
<div className="flex space-x-6">
<div
aria-label="Upload gas data"
className="w-full h-40 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center cursor-pointer"
role="button"
tabIndex={0}
onClick={() => document.getElementById('gas-upload')?.click()}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file) handleFileUpload('gas', file);
}}
onKeyDown={(e) => {
if ((e.key === 'Enter' || e.key === ' ') && gasFile) {
e.preventDefault();
}
}}
>
<p className="text-center p-4">Click or drag to upload gas bill PDF</p>
<input
accept=".pdf"
className="hidden"
id="gas-upload"
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleFileUpload('gas', file);
}}
/>
</div>
<div
className="w-full h-40 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center cursor-pointer"
role="button"
tabIndex={0}
onClick={() => document.getElementById('electricity-upload')?.click()}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file) handleFileUpload('electricity', file);
}}
onKeyDown={(e) => {
if ((e.key === 'Enter' || e.key === ' ') && electricityFile) {
e.preventDefault();
}
}}
>
<p className="text-center p-4">Click or drag to upload electricity bill PDF</p>
<input
accept=".pdf"
className="hidden"
id="electricity-upload"
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleFileUpload('electricity', file);
}}
/>
</div>
</div>
</ModalBody>
<ModalFooter />
</>
) : (
<>
<ModalHeader>Data Uploaded</ModalHeader>
<ModalBody>
<div className="flex flex-col items-center">
<p className="text-4xl"></p>
<p className="text-center mt-4">
Your file has been successfully uploaded! Please wait while we extract the data.
</p>
{(gasFile || electricityFile) && (
<Accordion className="w-full mt-4">
<AccordionItem key="1" aria-label="File Preview" title="File Preview">
{gasFile && gasFileUrl && (
<div>
<p>Gas Bill:</p>
<p>Name: {gasFile.name}</p>
<p>Type: {gasFile.type}</p>
<p>Size: {(gasFile.size / 1024).toFixed(2)} KB</p>
<embed
className="mt-2"
height="500px"
src={gasFileUrl}
type="application/pdf"
width="100%"
/>
</div>
)}
{electricityFile && electricityFileUrl && (
<div className="mt-4">
<p>Electricity Bill:</p>
<p>Name: {electricityFile.name}</p>
<p>Type: {electricityFile.type}</p>
<p>Size: {(electricityFile.size / 1024).toFixed(2)} KB</p>
<embed
className="mt-2"
height="500px"
src={electricityFileUrl}
type="application/pdf"
width="100%"
/>
</div>
)}
</AccordionItem>
<AccordionItem key="2" aria-label="Data Extraction" title="Data Extraction">
{extractionStatus === 'idle' && aiExtractionStatus === 'idle' && (
<div className="flex space-x-4">
<Button
color="primary"
onPress={handleExtraction}
>
Start Form Recognizer Extraction
</Button>
<Button
color="secondary"
onPress={handleAIExtraction}
>
Start AI-Powered Extraction
</Button>
</div>
)}
{extractionStatus === 'loading' && <p>Extracting data using Form Recognizer...</p>}
{aiExtractionStatus === 'loading' && <p>Extracting data using AI...</p>}
{extractionStatus === 'complete' && <p>Form Recognizer extraction complete!</p>}
{aiExtractionStatus === 'complete' && <p>AI-powered extraction complete!</p>}
</AccordionItem>
<AccordionItem key="3" aria-label="Data Preview" title="Data Preview">
{dataPreview ? (
<pre>{JSON.stringify(dataPreview, null, 2)}</pre>
) : (
<p>No data available. Please complete extraction first.</p>
)}
</AccordionItem>
</Accordion>
)}
</div>
</ModalBody>
<ModalFooter>
<Button color="primary" onPress={onClose}>
Close
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
);
}

11
Project/config/fonts.ts Executable file
View File

@@ -0,0 +1,11 @@
import { Fira_Code as FontMono, Inter as FontSans } from "next/font/google";
export const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
});
export const fontMono = FontMono({
subsets: ["latin"],
variable: "--font-mono",
});

9
Project/config/site.ts Executable file
View File

@@ -0,0 +1,9 @@
export type SiteConfig = typeof siteConfig;
export const siteConfig = {
name: "Carbin",
description: "Smart bins to help track your building's carbon footprint and encourage responsible waste management.",
links: {
github: "https://github.com/elibullockpapa/patriotHacks2024",
},
};

104
Project/config/trashItems.ts Executable file
View File

@@ -0,0 +1,104 @@
export const trashItems = [
{
id: "Aluminum-Can",
name: "Aluminum Can",
bin: "Recycling",
co2e: 170,
},
{
id: "Aluminum-Foil",
name: "Aluminum Foil",
bin: "Recycling",
note: "Please rinse and flatten",
co2e: 10,
},
{
id: "Bio-Plastic-Cup",
name: "Bio-Plastic Cup",
bin: "Compost",
co2e: 70,
},
{
id: "Cardboard",
name: "Cardboard",
bin: "Recycling",
note: "Please flatten all cardboard",
co2e: 80,
},
{
id: "Food",
name: "Food",
bin: "Compost",
co2e: 1000,
},
{
id: "Food-Wrapper",
name: "Food Wrapper",
bin: "Landfill",
co2e: 6,
},
{
id: "Paper",
name: "Paper",
bin: "Recycling",
co2e: 8,
},
{
id: "Paper-Cup",
name: "Paper Cup",
bin: "Recycling",
co2e: 11,
},
{
id: "Paper-Plate",
name: "Paper Plate",
bin: "Compost",
co2e: 15,
},
{
id: "Paper-Soft",
name: "Soft Paper",
bin: "Recycling",
co2e: 5,
},
{
id: "Plastic-Bag",
name: "Plastic Bag",
bin: "Landfill",
co2e: 33,
},
{
id: "Plastic-Bottle",
name: "Plastic Bottle",
bin: "Recycling",
note: "Only hard number 1 or 2 bottles",
co2e: 82,
},
{
id: "Plastic-Container",
name: "Plastic Container",
bin: "Recycling",
note: "Only hard plastics number 1 or 2",
co2e: 100,
},
{
id: "Plastic-Cup",
name: "Plastic Cup",
bin: "Recycling",
note: "Only hard plastics number 1 or 2",
co2e: 30,
},
{
id: "Plastic-Utensil",
name: "Plastic Utensil",
bin: "Landfill",
co2e: 8,
},
{
id: "Styrofoam",
name: "Styrofoam",
bin: "Landfill",
co2e: 45,
},
];

28
Project/lib/firebase.ts Executable file
View File

@@ -0,0 +1,28 @@
// lib/firebase.ts
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { initializeFirestore, persistentLocalCache, persistentMultipleTabManager } from "firebase/firestore";
// Firebase config object
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};
// Initialize Firebase app
const app = initializeApp(firebaseConfig);
// Initialize Firebase Auth
export const auth = getAuth(app);
// Initialize Firestore with offline persistence
export const db = initializeFirestore(app, {
localCache: persistentLocalCache({
tabManager: persistentMultipleTabManager(),
}),
});

145
Project/lib/useBuildingData.ts Executable file
View File

@@ -0,0 +1,145 @@
// lib/useBuildingData.ts
"use client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
export type ElectricityDataPoint = {
timestamp: {
seconds: number;
nanoseconds: number;
};
kwh: number;
emissions: number;
};
export type NaturalGasDataPoint = {
timestamp: {
seconds: number;
nanoseconds: number;
};
therms: number;
emissions: number;
};
export type WasteDataPoint = {
timestamp: {
seconds: number;
nanoseconds: number;
};
type: string;
trashcanID: string;
wasteCategory: string;
emissions: number;
};
export type Building = {
name: string;
id: string;
address: string;
yearBuilt: number;
squareFeet: number;
imageURL: string;
electricityUsage: Array<ElectricityDataPoint>;
naturalGasUsage: Array<NaturalGasDataPoint>;
wasteGeneration: Array<WasteDataPoint>;
}
const getBuildingsFromAPI = async () => {
const response = await fetch('/api/buildings');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
}
const updateBuildingInAPI = async (buildingId: string, newData: Partial<Building> & { operation?: string; index?: number }) => {
const response = await fetch('/api/buildings', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: buildingId, ...newData }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Network response was not ok: ${response.status} ${response.statusText}. ${JSON.stringify(errorData)}`);
}
return response.json();
}
export function useBuildingList() {
const query = useQuery<Building[], Error>({
queryKey: ['buildings'],
queryFn: getBuildingsFromAPI,
staleTime: 10 * 60 * 1000,
gcTime: 15 * 60 * 1000,
});
return {
...query,
};
}
export function useBuilding(buildingId: string) {
const queryClient = useQueryClient();
const query = useQuery<Building, Error>({
queryKey: ['building', buildingId],
queryFn: () => getBuildingFromAPI(buildingId),
staleTime: 10 * 60 * 1000,
gcTime: 15 * 60 * 1000,
});
const mutation = useMutation({
mutationFn: (data: Partial<Building> & { operation?: string; index?: number }) => updateBuildingInAPI(buildingId, data),
onMutate: async (data) => {
await queryClient.cancelQueries({ queryKey: ['building', buildingId] });
const previousBuilding = queryClient.getQueryData<Building>(['building', buildingId]);
queryClient.setQueryData<Building>(['building', buildingId], (oldData) => {
if (!oldData) return undefined;
if (data.operation === 'deleteWasteEntry' && typeof data.index === 'number') {
const newWasteGeneration = [...oldData.wasteGeneration];
newWasteGeneration.splice(data.index, 1);
return { ...oldData, wasteGeneration: newWasteGeneration };
}
return { ...oldData, ...data };
});
return { previousBuilding };
},
onError: (err, newData, context) => {
console.error("Error updating building data:", err);
// Log additional details about the failed update
console.error("Failed update data:", newData);
queryClient.setQueryData(['building', buildingId], context!.previousBuilding);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['building', buildingId] });
}
});
return {
...query,
updateBuilding: mutation.mutate
};
}
const getBuildingFromAPI = async (buildingId: string): Promise<Building> => {
const response = await fetch(`/api/buildings?id=${buildingId}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
}

13
Project/next.config.js Executable file
View File

@@ -0,0 +1,13 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '**',
},
],
},
}
module.exports = nextConfig

76
Project/package.json Executable file
View File

@@ -0,0 +1,76 @@
{
"name": "next-app-template",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "eslint . --ext .ts,.tsx -c .eslintrc.json --fix"
},
"dependencies": {
"@azure/ai-form-recognizer": "^5.0.0",
"@azure/cosmos": "^4.1.1",
"@azure/openai": "^2.0.0-beta.2",
"@hyzyla/pdfium": "^2.1.2",
"@nextui-org/button": "2.0.38",
"@nextui-org/code": "2.0.33",
"@nextui-org/input": "2.2.5",
"@nextui-org/kbd": "2.0.34",
"@nextui-org/link": "2.0.35",
"@nextui-org/listbox": "2.1.27",
"@nextui-org/navbar": "2.0.37",
"@nextui-org/react": "^2.4.8",
"@nextui-org/snippet": "2.0.43",
"@nextui-org/switch": "2.0.34",
"@nextui-org/system": "2.2.6",
"@nextui-org/theme": "2.2.11",
"@react-aria/ssr": "3.9.4",
"@react-aria/visually-hidden": "3.8.12",
"@tanstack/react-query": "^5.59.11",
"ai": "^3.4.9",
"axios": "^1.7.7",
"clsx": "2.1.1",
"firebase": "^10.14.1",
"form-data": "^4.0.1",
"formidable": "v3",
"framer-motion": "~11.1.9",
"inferencejs": "^1.0.13",
"intl-messageformat": "^10.6.0",
"next": "14.2.4",
"next-themes": "^0.2.1",
"openai": "^4.67.3",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-draggable": "^4.4.6",
"react-pdf": "^9.1.1",
"react-webcam": "^7.2.0",
"recharts": "^2.13.0",
"roboflow-js": "^0.2.32",
"screenfull": "^6.0.2",
"sharp": "^0.33.5"
},
"devDependencies": {
"@types/formidable": "^3.4.5",
"@types/node": "20.5.7",
"@types/react": "^18.3.11",
"@types/react-dom": "18.3.0",
"@typescript-eslint/eslint-plugin": "7.2.0",
"@typescript-eslint/parser": "7.2.0",
"autoprefixer": "10.4.19",
"eslint": "^8.57.1",
"eslint-config-next": "14.2.1",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^3.2.0",
"postcss": "8.4.38",
"tailwind-variants": "0.1.20",
"tailwindcss": "3.4.3",
"typescript": "5.0.4"
}
}

6
Project/postcss.config.js Executable file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
Project/public/carbin.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
Project/public/crumpled-paper.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
Project/public/demo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

BIN
Project/public/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
Project/public/homepage-video.mp4 Executable file

Binary file not shown.

23
Project/styles/globals.css Executable file
View File

@@ -0,0 +1,23 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.ripple-effect {
position: fixed;
width: 10px;
height: 10px;
border-radius: 50%;
transform: translate(-50%, -50%) scale(0);
animation: ripple-animation 1s ease-out forwards;
pointer-events: none;
z-index: 5;
}
}
@keyframes ripple-animation {
to {
transform: translate(-50%, -50%) scale(300);
opacity: 0;
}
}

22
Project/tailwind.config.js Executable file
View File

@@ -0,0 +1,22 @@
import { nextui } from '@nextui-org/theme'
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}'
],
theme: {
extend: {
fontFamily: {
sans: ["var(--font-sans)"],
mono: ["var(--font-mono)"],
baskerville: ["Libre Baskerville", "serif"],
},
},
},
darkMode: "class",
darkMode: "class",
plugins: [nextui()],
}

28
Project/tsconfig.json Executable file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

5
Project/types/index.ts Executable file
View File

@@ -0,0 +1,5 @@
import { SVGProps } from "react";
export type IconSvgProps = SVGProps<SVGSVGElement> & {
size?: number;
};

151
README.md Normal file
View File

@@ -0,0 +1,151 @@
# Patriot Hacks 2024
PatriotHacks 24 - 🏆 Microsoft X Cloudforce & 🏆 Save the World
## Repository overview
This repository contains the team's work for PatriotHacks 2024. It is divided into three main folders:
- `AI Training/` — Python scripts and small dataset utilities used to prepare and train ML models for trash classification.
- `File Converter/` — a small Node.js express service that converts PDF documents into images.
- `Project/` — a Next.js web application (called "Carbin") for tracking building carbon footprints and providing a trash-detection interface.
Below are concise descriptions and quick start instructions for each subfolder so you (or another team member) can run or continue development.
## AI Training/
Purpose
- Utilities and small scripts used to: extract images from a HF parquet dataset, upload images to Azure Custom Vision, and run model inference examples.
Key files
- `hf.py` — shows how to load a Hugging Face image classification model (AutoImageProcessor + AutoModelForImageClassification) for inference.
- `train.py` — script that uploads images from a Hugging Face parquet dataset into an Azure Custom Vision project and triggers training. NOTE: the current script contains hard-coded Azure endpoint/API keys and project IDs; replace these with environment variables before sharing or running.
- `ml.py` — extracts images from a Hugging Face parquet dataset and writes images into the `AI Training/images/` folders by label (biodegradable, cardboard, glass, metal, paper, plastic).
- `main.py` — another example showing uploading images to Custom Vision and iterating the dataset to create images by tag.
- `pdf.py` — (small utility) sample code to work with pdf-to-image flows referenced in other parts of the project.
Quick start (local, Python)
1. Create and activate a virtual environment (recommended):
```bash
python -m venv .venv
source .venv/bin/activate
```
2. Install common dependencies used by the scripts (adjust versions as needed):
```bash
pip install dask[complete] azure-cognitiveservices-vision-customvision msrest transformers opencv-python
```
3. Important: set Azure Custom Vision credentials and the project id as environment variables or edit the scripts to load them securely (do not commit keys):
```bash
export AZURE_CUSTOMVISION_ENDPOINT="https://..."
export AZURE_CUSTOMVISION_TRAINING_KEY="<your-training-key>"
export AZURE_CUSTOMVISION_PREDICTION_KEY="<your-prediction-key>"
export CUSTOMVISION_PROJECT_ID="<project-id>"
```
4. Run the scripts as needed. For example, to extract images from the HF parquet source:
```bash
python "AI Training/ml.py"
```
Notes and security
- Some scripts currently contain hard-coded keys and endpoints. Replace them with environment variables or a secrets store before running in any shared environment.
- The code expects a Hugging Face dataset accessible via an `hf://` parquet path. Ensure you have appropriate HF credentials or local parquet files when running.
## File Converter/
Purpose
- Small Node.js Express service that accepts an uploaded PDF and returns PNG/JPEG frames generated via `pdf-to-img`.
Key files
- `index.js` — Express server exposing `/convert` POST endpoint that accepts a file upload (`express-fileupload`) and converts the PDF into images with `pdf-to-img`.
- `package.json` — lists dependencies required by the service.
Quick start (Node.js)
1. From the `File Converter` folder, install dependencies:
```bash
cd "File Converter"
npm install
```
2. Run the server (the `package.json` in this folder does not define a start script, so run node directly):
```bash
node index.js
```
3. The server listens on port 5000 (see `index.js`). POST a `multipart/form-data` request with the `pdf` file field to `http://localhost:5000/convert` to receive converted images in the response.
Notes
- Consider adding a `start` script to `package.json` and proper error handling and file-size limits before production use.
## Project/ (Carbin)
Purpose
- A Next.js (v14) application named "Carbin" for tracking building carbon footprints. It includes:
- Realtime trash detection UI using an inference worker and webcam access
- Building list and detail pages
- Firebase-backed data persistence for building metrics
Key files & directories (high level)
- `package.json` — lists dependencies and provides `dev`, `build`, and `start` scripts.
- `app/` — Next.js app routes and pages including API routes under `app/api/` (e.g. `buildings`, `chat`, `pdf-to-image`).
- `components/` — reusable UI components (navbar, trash-detection UI, theme switch, etc.).
- `lib/firebase.ts` — Firebase initialization (reads config from `NEXT_PUBLIC_*` env vars).
- `lib/useBuildingData.ts` — React Query hooks for fetching and updating building data.
- `config/` — app config (site metadata and trash item definitions used for co2 calculations).
Quick start (Next.js)
1. From the `Project/` folder, install dependencies and run dev:
```bash
cd Project
npm install
npm run dev
```
2. Required environment variables
- The app uses Firebase and expects the following env vars (set these before running):
```bash
export NEXT_PUBLIC_FIREBASE_API_KEY="..."
export NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="..."
export NEXT_PUBLIC_FIREBASE_PROJECT_ID="..."
export NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="..."
export NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="..."
export NEXT_PUBLIC_FIREBASE_APP_ID="..."
```
3. Visit `http://localhost:3000` after running `npm run dev`.
Notes and next steps
- The project contains an inference worker integration (`inferencejs`) used by the `trashDetection` component to run a model in the browser/worker; you will need the correct worker name/id and API key for that service.
- The `config/trashItems.ts` file contains per-item CO2e estimates used by the app — adjust values as appropriate.
## Contribution & development tips
- Replace any hard-coded secrets (Azure keys, Firebase secrets) with environment variables or a vault before committing.
- Add `start`/`dev` scripts where missing (e.g., the `File Converter` folder) for a consistent DX.
- Consider adding minimal README files inside each subfolder (AI Training/, File Converter/, Project/) if you plan to expand the team.
## Contact / Source
Repository maintained for PatriotHacks 2024. The Next.js app links to the GitHub org in `Project/config/site.ts`.
---
If you want, I can:
- add per-folder README files
- create a `requirements.txt` for the Python scripts and a `start` script for the File Converter
- remove or externalize hard-coded secrets into env var usage in the Python scripts
Tell me which of those you'd like next and I'll implement it.