commit f099f368383dbbf65754d62b2e6448a24dde35d8 Author: GamerBoss101 Date: Fri Oct 24 02:07:59 2025 -0400 Initial Code Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf9fc4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +images/ +File Converter/pnpm-lock.yaml diff --git a/AI Training/ai.ipynb b/AI Training/ai.ipynb new file mode 100755 index 0000000..c067072 --- /dev/null +++ b/AI Training/ai.ipynb @@ -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 +} diff --git a/AI Training/electricity-sample-bill.pdf b/AI Training/electricity-sample-bill.pdf new file mode 100755 index 0000000..33247ba Binary files /dev/null and b/AI Training/electricity-sample-bill.pdf differ diff --git a/AI Training/hf.py b/AI Training/hf.py new file mode 100755 index 0000000..b6d581a --- /dev/null +++ b/AI Training/hf.py @@ -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") + diff --git a/AI Training/main.py b/AI Training/main.py new file mode 100755 index 0000000..05bfdbf --- /dev/null +++ b/AI Training/main.py @@ -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") diff --git a/AI Training/ml.py b/AI Training/ml.py new file mode 100755 index 0000000..cd75f0d --- /dev/null +++ b/AI Training/ml.py @@ -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!") + + diff --git a/AI Training/pdf.py b/AI Training/pdf.py new file mode 100755 index 0000000..abaefc2 --- /dev/null +++ b/AI Training/pdf.py @@ -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) \ No newline at end of file diff --git a/AI Training/train.py b/AI Training/train.py new file mode 100755 index 0000000..a530330 --- /dev/null +++ b/AI Training/train.py @@ -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!") + diff --git a/File Converter/index.js b/File Converter/index.js new file mode 100755 index 0000000..fe52b49 --- /dev/null +++ b/File Converter/index.js @@ -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'); +}); \ No newline at end of file diff --git a/File Converter/package.json b/File Converter/package.json new file mode 100755 index 0000000..3726d08 --- /dev/null +++ b/File Converter/package.json @@ -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" + } +} diff --git a/Project/.dockerignore b/Project/.dockerignore new file mode 100755 index 0000000..7b2b00b --- /dev/null +++ b/Project/.dockerignore @@ -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 + + diff --git a/Project/.eslintignore b/Project/.eslintignore new file mode 100755 index 0000000..d1804c2 --- /dev/null +++ b/Project/.eslintignore @@ -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 \ No newline at end of file diff --git a/Project/.eslintrc.json b/Project/.eslintrc.json new file mode 100755 index 0000000..21fae6b --- /dev/null +++ b/Project/.eslintrc.json @@ -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" + ] + } + ] + } +} \ No newline at end of file diff --git a/Project/.gitignore b/Project/.gitignore new file mode 100755 index 0000000..9802054 --- /dev/null +++ b/Project/.gitignore @@ -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 diff --git a/Project/Dockerfile b/Project/Dockerfile new file mode 100755 index 0000000..32884e6 --- /dev/null +++ b/Project/Dockerfile @@ -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"] diff --git a/Project/README.md b/Project/README.md new file mode 100755 index 0000000..8924ce0 --- /dev/null +++ b/Project/README.md @@ -0,0 +1 @@ +An app for tracking the carbon footprint of buildings. \ No newline at end of file diff --git a/Project/app/api/buildings/route.ts b/Project/app/api/buildings/route.ts new file mode 100755 index 0000000..57e2c7f --- /dev/null +++ b/Project/app/api/buildings/route.ts @@ -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 }); + } +} diff --git a/Project/app/api/chat/route.ts b/Project/app/api/chat/route.ts new file mode 100755 index 0000000..68ba148 --- /dev/null +++ b/Project/app/api/chat/route.ts @@ -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": "", + "usage": + }, + // ... 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 }); + } +} + diff --git a/Project/app/api/pdf-to-image/route.ts b/Project/app/api/pdf-to-image/route.ts new file mode 100755 index 0000000..375ff2e --- /dev/null +++ b/Project/app/api/pdf-to-image/route.ts @@ -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 }); + } +} diff --git a/Project/app/buildings/[buildingid]/emissions/page.tsx b/Project/app/buildings/[buildingid]/emissions/page.tsx new file mode 100755 index 0000000..beadcd4 --- /dev/null +++ b/Project/app/buildings/[buildingid]/emissions/page.tsx @@ -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(null); + const [endDate, setEndDate] = useState(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 ( +
+ + {/* Tab Title */} +

+ {`Emissions`} +

+ + + {/* Group for filters plus graph */} +
+ {/* Horizontal group for adding data and filters */} + + +
+ {/* Data Type Selection Card */} + + +

Data Types

+
+ +
+ + + +
+
+
+ + {/* Chart Type Selection Card */} + + +

Chart Type

+
+ + + + + + + +
+ + {/* Date Range Selection Card */} + + +

Date Range

+
+ +
+ + + + + + + + + + + + + + + + +
+
+
+
+ + + + {/* Render emissions graph */} + +
+
+ ); +} diff --git a/Project/app/buildings/[buildingid]/layout.tsx b/Project/app/buildings/[buildingid]/layout.tsx new file mode 100755 index 0000000..0c715e4 --- /dev/null +++ b/Project/app/buildings/[buildingid]/layout.tsx @@ -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 ( +
+ +
{children}
+
+ ); +} diff --git a/Project/app/buildings/[buildingid]/page.tsx b/Project/app/buildings/[buildingid]/page.tsx new file mode 100755 index 0000000..5a59ba4 --- /dev/null +++ b/Project/app/buildings/[buildingid]/page.tsx @@ -0,0 +1,13 @@ +// app/buildings/[buildingid]/page.tsx + +interface BuildingPageProps { + params: { buildingid: string }; +} + +export default function BuildingPage({ params }: BuildingPageProps) { + return ( +
+ Select a tab to view information about this building. +
+ ); +} diff --git a/Project/app/buildings/[buildingid]/trash-scanner/oldpage.tsx b/Project/app/buildings/[buildingid]/trash-scanner/oldpage.tsx new file mode 100755 index 0000000..d6955a4 --- /dev/null +++ b/Project/app/buildings/[buildingid]/trash-scanner/oldpage.tsx @@ -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(null); + const canvasRef = useRef(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(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) => { + 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) => { + 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 ( +
+

Trash Scanner

+
+ + +
+ + +
+
+
+ +
+
+ ); +}; + +export default TrashScanner; diff --git a/Project/app/buildings/[buildingid]/trash-scanner/page.tsx b/Project/app/buildings/[buildingid]/trash-scanner/page.tsx new file mode 100755 index 0000000..aab708f --- /dev/null +++ b/Project/app/buildings/[buildingid]/trash-scanner/page.tsx @@ -0,0 +1,7 @@ +// app/buildings/[buildingid]/trash-scanner/page.tsx + +import RealtimeModel from "@/components/trashDetection"; + +export default function TrashScanner() { + return ; +}; diff --git a/Project/app/buildings/[buildingid]/trash/page.tsx b/Project/app/buildings/[buildingid]/trash/page.tsx new file mode 100755 index 0000000..8c2b24f --- /dev/null +++ b/Project/app/buildings/[buildingid]/trash/page.tsx @@ -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
Loading...
; + if (error) return
Error: {error.message}
; + if (!building) return
Building not found
; + + const handleInputChange = (e: React.ChangeEvent) => { + 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 ( +
+

Waste Data for {building?.name}

+ + + handleSort('timestamp')}>Timestamp + handleSort('wasteCategory')}>Name + handleSort('type')}>Trash Category + handleSort('trashcanID')}>Trashcan ID + handleSort('emissions')}>Emissions (kg ofCO2e) + Actions + + + {sortedWasteGeneration.map((wastePoint, index) => ( + + {new Date(wastePoint.timestamp.seconds * 1000).toLocaleString()} + {wastePoint.wasteCategory} + {wastePoint.type} + {wastePoint.trashcanID} + {(wastePoint.emissions * 1e+3).toFixed(0)} + + + + + ))} + +
+ + + + setIsModalOpen(false)}> + + +

Add New Waste Entry

+
+ + + + + + + + + + + +
+
+
+ ); +} diff --git a/Project/app/buildings/[buildingid]/trashcan-mode/page.tsx b/Project/app/buildings/[buildingid]/trashcan-mode/page.tsx new file mode 100755 index 0000000..8397d45 --- /dev/null +++ b/Project/app/buildings/[buildingid]/trashcan-mode/page.tsx @@ -0,0 +1,7 @@ +// app/buildings/[buildingid]/trashcan-mode/page.tsx + +import TrashcanMode from "@/components/trashcanMode"; + +export default function TrashcanModePage() { + return ; +} \ No newline at end of file diff --git a/Project/app/buildings/page.tsx b/Project/app/buildings/page.tsx new file mode 100755 index 0000000..a25f3ac --- /dev/null +++ b/Project/app/buildings/page.tsx @@ -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 ( +
+ {[...Array(3)].map((_, index) => ( + + +
+ + + ))} +
+ ); + if (error) return
Error: {error.message}
; + + if (buildings) return ( +
+ {buildings.map(building => ( + + +

{building.name}

+

{building.address}

+
+ {`${building.name} + +
+

Year Built: {building.yearBuilt}

+

Square Footage: {building.squareFeet.toLocaleString()}

+
+ + + +
+
+ ))} +
+ ); + + return
No buildings found
; +} diff --git a/Project/app/error.tsx b/Project/app/error.tsx new file mode 100755 index 0000000..0af470a --- /dev/null +++ b/Project/app/error.tsx @@ -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 ( +
+

Something went wrong!

+ +
+ ); +} diff --git a/Project/app/featureBox.tsx b/Project/app/featureBox.tsx new file mode 100755 index 0000000..3340c9f --- /dev/null +++ b/Project/app/featureBox.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +interface FeatureBoxProps { + title: string; + description: string; + theme?: string; +} + +const FeatureBox: React.FC = ({ title, description, theme }) => { + const isDarkMode = theme === 'dark'; + + return ( +
+

+ {title} +

+

+ {description} +

+
+ ); +}; + +export default FeatureBox; diff --git a/Project/app/layout.tsx b/Project/app/layout.tsx new file mode 100755 index 0000000..f1876f3 --- /dev/null +++ b/Project/app/layout.tsx @@ -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 ( + + + + +
+ +
+ {children} +
+
+
+ + + ); +} diff --git a/Project/app/mission/page.tsx b/Project/app/mission/page.tsx new file mode 100755 index 0000000..2f3afee --- /dev/null +++ b/Project/app/mission/page.tsx @@ -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 ( +
+ {/* Large "About Us" text at the top */} +
+

Our Mission

+
+ +
+ Demo image +
+ {/* text at the bottom */} +
+

+ One of the most neglected aspects of a building's carbon footprint is waste mismanagement and poor recycling practices. According to the EPA, landfills account for 15% of U.S. methane emissions, with commercial buildings generating over 30% of the nation's total waste. +
+
+ Studies show that up to 25% of items 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's carbon emissions by up to 40%. +
+
+ Carbin 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 educate building occupants, 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. +

+ +
+
+
+
+ ); +} diff --git a/Project/app/page.tsx b/Project/app/page.tsx new file mode 100755 index 0000000..22cc07a --- /dev/null +++ b/Project/app/page.tsx @@ -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 ( +
+
+
+ +
+
+
+
+ + Your platform for{" "} + + + building + +
+ + a sustainable future + +
+ +
+ Encourage student participation in responsible waste management with smart bins that guide proper disposal. +
+ +
+ + Get Started + +
+
+
+ +
+
+
+ + + +
+
+
+
+ ); +} diff --git a/Project/app/providers.tsx b/Project/app/providers.tsx new file mode 100755 index 0000000..d6c9715 --- /dev/null +++ b/Project/app/providers.tsx @@ -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 ( + + + {children} + + + ); +} diff --git a/Project/components/addDataButton.tsx b/Project/components/addDataButton.tsx new file mode 100755 index 0000000..1d9dca3 --- /dev/null +++ b/Project/components/addDataButton.tsx @@ -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 ( + <> + + setIsModalOpen(false)} + /> + + ); +} \ No newline at end of file diff --git a/Project/components/emissionsGraph.tsx b/Project/components/emissionsGraph.tsx new file mode 100755 index 0000000..6c9291f --- /dev/null +++ b/Project/components/emissionsGraph.tsx @@ -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>(); + + 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(); + 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 = { 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(); + + 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 ( +
+ {/* Skeleton content */} +
+ Loading... +
+
+ ); + } + if (error) return
Error: {error.message}
; + + const renderLineChart = () => ( + + + + + Number(value).toFixed(3)} /> + + {filters.showElectricity && building && building.electricityUsage.length > 0 && + } + {filters.showGas && building && building.naturalGasUsage.length > 0 && + } + {filters.showWaste && building && building.wasteGeneration.length > 0 && + } + + ); + + const renderAreaChart = () => ( + + + + + Number(value).toFixed(3)} /> + + {filters.showElectricity && building && building.electricityUsage.length > 0 && + } + {filters.showGas && building && building.naturalGasUsage.length > 0 && + } + {filters.showWaste && building && building.wasteGeneration.length > 0 && + } + + ); + + const renderPieChart = () => ( + + `${name} ${(percent * 100).toFixed(0)}%`} + > + {pieChartData.map((entry, index) => ( + + ))} + + + + + ); + + return ( +
+ + {(() => { + switch (graphType) { + case 'line': + return renderLineChart(); + case 'area': + return renderAreaChart(); + case 'pie': + return renderPieChart(); + default: + return <>; + } + })()} + +
+ ); +} diff --git a/Project/components/face.tsx b/Project/components/face.tsx new file mode 100755 index 0000000..977cd22 --- /dev/null +++ b/Project/components/face.tsx @@ -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 = ({ bin, isVisible, itemPosition, videoDimensions, facePosition }) => { + // Calculate eye rotation based on item position + const [eyeRotation, setEyeRotation] = useState(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 ( + +
+ {/* Face SVG or Graphics */} +
+
+
+
+
+ + ); +}; + +export default Face; \ No newline at end of file diff --git a/Project/components/icons.tsx b/Project/components/icons.tsx new file mode 100755 index 0000000..70c1446 --- /dev/null +++ b/Project/components/icons.tsx @@ -0,0 +1,230 @@ +import * as React from "react"; + +import { IconSvgProps } from "@/types"; + +export const GithubIcon: React.FC = ({ + size = 24, + width, + height, + ...props +}) => { + return ( + + + + ); +}; + +export const MoonFilledIcon = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + +); + +export const SunFilledIcon = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + +); + +export const HeartFilledIcon = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + +); + +export const SearchIcon = (props: IconSvgProps) => ( + +); + +export const LoadingCircleIcon = ({ + size = 24, + className, + ...props +}: IconSvgProps & { size?: number; className?: string }) => ( + + + + +); + +export const LeftArrowIcon = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + + + +); + +export const SettingsIcon = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + + + +); + + +export const PlusIcon = ({ + size = 24, + width, + height, + ...props +}: IconSvgProps) => ( + + + +); + + + diff --git a/Project/components/navbar.tsx b/Project/components/navbar.tsx new file mode 100755 index 0000000..a94ebdd --- /dev/null +++ b/Project/components/navbar.tsx @@ -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 ( + + + + + Carbin Logo + +

Carbin

+
+
+
+ + + + + + + + + + + + + + + + + + +
+ ); +}; diff --git a/Project/components/primitives.ts b/Project/components/primitives.ts new file mode 100755 index 0000000..e8ae11c --- /dev/null +++ b/Project/components/primitives.ts @@ -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, + }, +}); diff --git a/Project/components/sidebar.tsx b/Project/components/sidebar.tsx new file mode 100755 index 0000000..65f7b98 --- /dev/null +++ b/Project/components/sidebar.tsx @@ -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 ( +
+ + {/* Top section with info about building */} +
+ + {/* Back to all buildings */} + + + + + {/* Photo of building */} + {isLoading ? ( + +
+ + ) : error ? ( +
Error: {error.message}
+ ) : !buildingData ? ( +
No building found
+ ) : ( + + )} + + {/* Name of building and settings button*/} + {isLoading ? ( + +
+ + ) : buildingData ? ( +
+

{buildingData.name}

+
+ ) : null} +
+ + {/* Middle section with navigation links */} + + + {/* Bottom section with quick actions */} +
+ +
{/* Vertical divider */} + + + + {/*
+ + + */} +
+
+ ); +} diff --git a/Project/components/theme-switch.tsx b/Project/components/theme-switch.tsx new file mode 100755 index 0000000..897bfab --- /dev/null +++ b/Project/components/theme-switch.tsx @@ -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 = ({ + 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 ( + + + + +
+ {!isSelected || isSSR ? ( + + ) : ( + + )} +
+
+ ); +}; diff --git a/Project/components/trashDetection.tsx b/Project/components/trashDetection.tsx new file mode 100755 index 0000000..3f0017e --- /dev/null +++ b/Project/components/trashDetection.tsx @@ -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(null); + const modelWorkerIdRef = useRef(null); + const [modelLoading, setModelLoading] = useState(false); + const [predictions, setPredictions] = useState([]); + const containerRef = useRef(null); + + const videoRef = useRef(null); + const canvasRef = useRef(null); + + // References to manage media stream and timeouts + const mediaStreamRef = useRef(null); + const detectFrameTimeoutRef = useRef(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 ( +
+
+ + +
+
+
+ {predictions.length > 0 ? ( + predictions.map((prediction, index) => ( +
+ {`${prediction.class} - ${Math.round(prediction.confidence * 100)}%`} +
+ )) + ) : ( +
No Item Detected
+ )} +
+
+
+ ); +} + +export default RealtimeModel; \ No newline at end of file diff --git a/Project/components/trashcanMode.tsx b/Project/components/trashcanMode.tsx new file mode 100755 index 0000000..3eca521 --- /dev/null +++ b/Project/components/trashcanMode.tsx @@ -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(null); + const [modelLoading, setModelLoading] = useState(false); + const [currentItem, setCurrentItem] = useState(null); // Current detected item + const [thrownItems, setThrownItems] = useState([]); // 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(''); + const [ripplePosition, setRipplePosition] = useState<{ x: string; y: string }>({ x: '50%', y: '50%' }); + + // References to DOM elements + const videoRef = useRef(null); + const canvasRef = useRef(null); + const containerRef = useRef(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 ( +
+ {/* Hidden video element for capturing webcam feed */} + + + {/* Video and canvas elements for display */} + {showCamera && ( +
+ + +
+ )} + + {/* Ripple Effect Overlay */} + {rippleActive && ( +
+ )} + + {/* Main content */} +
+ {showCelebration ? ( + + + ✅ + +

+ Great job! +

+
+ ) : currentItem ? ( + +

+ {getBinEmoji(currentItem.bin)} {currentItem.bin} {getArrow(currentItem.bin)} +

+

+ {getItemEmoji(currentItem.id)} {currentItem.name} +

+ {currentItem.note && ( +

+ {currentItem.note} +

+ )} +
+ ) : ( + +

+ No Item Detected +

+ {thrownItems.length > 0 && ( +
+

+ Recently Thrown Items: +

+

+ {Object.entries( + thrownItems.slice(-5).reduce((acc, item) => { + acc[item] = (acc[item] || 0) + 1; + return acc; + }, {} as Record) + ) + .map(([item, count]) => (count > 1 ? `${item} (${count}x)` : item)) + .join(", ")} +

+
+ )} +
+ )} +
+
+ ); +} + +export default TrashcanMode; \ No newline at end of file diff --git a/Project/components/uploadDataModal.tsx b/Project/components/uploadDataModal.tsx new file mode 100755 index 0000000..1b2b14a --- /dev/null +++ b/Project/components/uploadDataModal.tsx @@ -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(null); + const [electricityFile, setElectricityFile] = useState(null); + const [gasFileUrl, setGasFileUrl] = useState(null); + const [electricityFileUrl, setElectricityFileUrl] = useState(null); + const [extractionStatus, setExtractionStatus] = useState<'idle' | 'loading' | 'complete'>('idle'); + const [aiExtractionStatus, setAiExtractionStatus] = useState<'idle' | 'loading' | 'complete'>('idle'); + const [dataPreview, setDataPreview] = useState(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 ( + + + {!isSubmitted ? ( + <> + Upload New Data + +
+
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(); + } + }} + > +

Click or drag to upload gas bill PDF

+ { + const file = e.target.files?.[0]; + + if (file) handleFileUpload('gas', file); + }} + /> +
+
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(); + } + }} + > +

Click or drag to upload electricity bill PDF

+ { + const file = e.target.files?.[0]; + + if (file) handleFileUpload('electricity', file); + }} + /> +
+
+
+ + + ) : ( + <> + Data Uploaded + +
+

+

+ Your file has been successfully uploaded! Please wait while we extract the data. +

+ {(gasFile || electricityFile) && ( + + + {gasFile && gasFileUrl && ( +
+

Gas Bill:

+

Name: {gasFile.name}

+

Type: {gasFile.type}

+

Size: {(gasFile.size / 1024).toFixed(2)} KB

+ +
+ )} + {electricityFile && electricityFileUrl && ( +
+

Electricity Bill:

+

Name: {electricityFile.name}

+

Type: {electricityFile.type}

+

Size: {(electricityFile.size / 1024).toFixed(2)} KB

+ +
+ )} +
+ + {extractionStatus === 'idle' && aiExtractionStatus === 'idle' && ( +
+ + +
+ )} + {extractionStatus === 'loading' &&

Extracting data using Form Recognizer...

} + {aiExtractionStatus === 'loading' &&

Extracting data using AI...

} + {extractionStatus === 'complete' &&

Form Recognizer extraction complete!

} + {aiExtractionStatus === 'complete' &&

AI-powered extraction complete!

} +
+ + {dataPreview ? ( +
{JSON.stringify(dataPreview, null, 2)}
+ ) : ( +

No data available. Please complete extraction first.

+ )} +
+
+ )} +
+
+ + + + + )} +
+
+ ); +} \ No newline at end of file diff --git a/Project/config/fonts.ts b/Project/config/fonts.ts new file mode 100755 index 0000000..569c245 --- /dev/null +++ b/Project/config/fonts.ts @@ -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", +}); diff --git a/Project/config/site.ts b/Project/config/site.ts new file mode 100755 index 0000000..9392c74 --- /dev/null +++ b/Project/config/site.ts @@ -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", + }, +}; diff --git a/Project/config/trashItems.ts b/Project/config/trashItems.ts new file mode 100755 index 0000000..cb914e4 --- /dev/null +++ b/Project/config/trashItems.ts @@ -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, + }, +]; \ No newline at end of file diff --git a/Project/lib/firebase.ts b/Project/lib/firebase.ts new file mode 100755 index 0000000..e3d4795 --- /dev/null +++ b/Project/lib/firebase.ts @@ -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(), + }), +}); diff --git a/Project/lib/useBuildingData.ts b/Project/lib/useBuildingData.ts new file mode 100755 index 0000000..1c94797 --- /dev/null +++ b/Project/lib/useBuildingData.ts @@ -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; + naturalGasUsage: Array; + wasteGeneration: Array; +} + +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 & { 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({ + 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({ + queryKey: ['building', buildingId], + queryFn: () => getBuildingFromAPI(buildingId), + staleTime: 10 * 60 * 1000, + gcTime: 15 * 60 * 1000, + }); + + const mutation = useMutation({ + mutationFn: (data: Partial & { operation?: string; index?: number }) => updateBuildingInAPI(buildingId, data), + onMutate: async (data) => { + await queryClient.cancelQueries({ queryKey: ['building', buildingId] }); + const previousBuilding = queryClient.getQueryData(['building', buildingId]); + + queryClient.setQueryData(['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 => { + const response = await fetch(`/api/buildings?id=${buildingId}`); + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + return response.json(); +} diff --git a/Project/next.config.js b/Project/next.config.js new file mode 100755 index 0000000..6b9304f --- /dev/null +++ b/Project/next.config.js @@ -0,0 +1,13 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '**', + }, + ], + }, +} + +module.exports = nextConfig diff --git a/Project/package.json b/Project/package.json new file mode 100755 index 0000000..1db9111 --- /dev/null +++ b/Project/package.json @@ -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" + } +} diff --git a/Project/postcss.config.js b/Project/postcss.config.js new file mode 100755 index 0000000..a03e681 --- /dev/null +++ b/Project/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/Project/public/carbin.png b/Project/public/carbin.png new file mode 100755 index 0000000..643557a Binary files /dev/null and b/Project/public/carbin.png differ diff --git a/Project/public/crumpled-paper.png b/Project/public/crumpled-paper.png new file mode 100755 index 0000000..7c76238 Binary files /dev/null and b/Project/public/crumpled-paper.png differ diff --git a/Project/public/demo.png b/Project/public/demo.png new file mode 100755 index 0000000..668f914 Binary files /dev/null and b/Project/public/demo.png differ diff --git a/Project/public/electricity-sample-bill.pdf b/Project/public/electricity-sample-bill.pdf new file mode 100755 index 0000000..33247ba Binary files /dev/null and b/Project/public/electricity-sample-bill.pdf differ diff --git a/Project/public/favicon.ico b/Project/public/favicon.ico new file mode 100755 index 0000000..643557a Binary files /dev/null and b/Project/public/favicon.ico differ diff --git a/Project/public/homepage-video.mp4 b/Project/public/homepage-video.mp4 new file mode 100755 index 0000000..df960a6 Binary files /dev/null and b/Project/public/homepage-video.mp4 differ diff --git a/Project/styles/globals.css b/Project/styles/globals.css new file mode 100755 index 0000000..44114b1 --- /dev/null +++ b/Project/styles/globals.css @@ -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; + } +} \ No newline at end of file diff --git a/Project/tailwind.config.js b/Project/tailwind.config.js new file mode 100755 index 0000000..7708ff6 --- /dev/null +++ b/Project/tailwind.config.js @@ -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()], +} diff --git a/Project/tsconfig.json b/Project/tsconfig.json new file mode 100755 index 0000000..0c767fb --- /dev/null +++ b/Project/tsconfig.json @@ -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"] +} diff --git a/Project/types/index.ts b/Project/types/index.ts new file mode 100755 index 0000000..533a827 --- /dev/null +++ b/Project/types/index.ts @@ -0,0 +1,5 @@ +import { SVGProps } from "react"; + +export type IconSvgProps = SVGProps & { + size?: number; +}; diff --git a/README.md b/README.md new file mode 100644 index 0000000..5333ce6 --- /dev/null +++ b/README.md @@ -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="" +export AZURE_CUSTOMVISION_PREDICTION_KEY="" +export CUSTOMVISION_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. \ No newline at end of file