From a2b7d26759b959ed23075f689c826a969d239f69 Mon Sep 17 00:00:00 2001 From: Sharanya Basu Date: Thu, 20 Aug 2026 23:43:23 -0400 Subject: [PATCH 1/4] feat: redesign home page layout and interactions --- src/components/TechSlideshow.tsx | 3 +- src/pages/Home.tsx | 268 ++++++++++++++++++++++++++----- 2 files changed, 227 insertions(+), 44 deletions(-) diff --git a/src/components/TechSlideshow.tsx b/src/components/TechSlideshow.tsx index d378c85..295713b 100644 --- a/src/components/TechSlideshow.tsx +++ b/src/components/TechSlideshow.tsx @@ -15,10 +15,11 @@ const moveSlideshow = keyframes` // Moving background sprite container const Mover = styled(Box)(({ theme }) => ({ - marginTop: "-50px", height: "100%", width: "3184px", backgroundImage: `url(${require("../assets/slider3_opt.png")})`, + backgroundPosition: "center center", + backgroundRepeat: "repeat-x", position: "absolute", top: 0, left: 0, diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 4605342..d08e049 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -2,20 +2,92 @@ * Home - Main landing page with hero, about, stats, partners, and CTA sections * Update stats array and section content as needed */ -import React, { memo } from "react"; +import React, { memo, useEffect, useRef, useState } from "react"; import Box from "@mui/material/Box"; +import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded"; import { ReactComponent as Logo } from "../assets/wat_ai_logo.svg"; import TechSlideshow from "../components/TechSlideshow"; import { useTheme } from "@mui/material/styles"; import { HeroTitle, SectionTitle, SubsectionTitle, BodyLarge, BodyText } from "../components/Typography"; import UnifiedSection from "../components/UnifiedSection"; -import UnifiedCard from "../components/UnifiedCard"; import UnifiedButton from "../components/UnifiedButton"; -import UnifiedStats from "../components/UnifiedStats"; + +const AnimatedCounter: React.FC<{ value: string }> = ({ value }) => { + const target = Number.parseInt(value, 10); + const suffix = value.replace(/^\d+/, ""); + const [count, setCount] = useState(0); + const counterRef = useRef(null); + + useEffect(() => { + const element = counterRef.current; + if (!element || Number.isNaN(target)) return; + + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + setCount(target); + return; + } + + let animationFrame = 0; + const observer = new IntersectionObserver( + ([entry]) => { + if (!entry.isIntersecting) return; + + const duration = 1400; + const startTime = performance.now(); + + const animate = (now: number) => { + const progress = Math.min((now - startTime) / duration, 1); + const easedProgress = 1 - Math.pow(1 - progress, 3); + setCount(Math.round(target * easedProgress)); + + if (progress < 1) { + animationFrame = window.requestAnimationFrame(animate); + } + }; + + animationFrame = window.requestAnimationFrame(animate); + observer.disconnect(); + }, + { threshold: 0.35 } + ); + + observer.observe(element); + + return () => { + observer.disconnect(); + window.cancelAnimationFrame(animationFrame); + }; + }, [target]); + + return ( + + {count}{suffix} + + ); +}; const HomePage: React.FC = memo(() => { const theme = useTheme(); + const sectionTitleSx = { + color: theme.palette.text.primary, + fontSize: { xs: "2.2rem", sm: "2.8rem", md: "3.35rem" }, + fontWeight: 750, + lineHeight: 1.1, + letterSpacing: "-0.04em", + mb: { xs: 6, md: 8 }, + position: "relative", + "&::after": { + content: '""', + display: "block", + width: 72, + height: 2, + mt: 2.5, + mx: "auto", + background: `linear-gradient(90deg, transparent, ${theme.palette.primary.main}, transparent)`, + }, + }; + // Key metrics displayed on homepage const stats = [ { number: "450+", label: "Program Graduates", description: "Students & researchers" }, @@ -35,28 +107,52 @@ const HomePage: React.FC = memo(() => { {/* Hero Section */} - + Fostering the Future of AI at Waterloo @@ -64,7 +160,7 @@ const HomePage: React.FC = memo(() => { variant="primary" size="large" to="/students" - endIcon={} + endIcon={} > Get Involved @@ -74,21 +170,37 @@ const HomePage: React.FC = memo(() => { {/* About Section */} - - + + About Us - Fostering The Future Of AI Talent At The University of Waterloo @@ -96,8 +208,10 @@ const HomePage: React.FC = memo(() => { textAlign: "center", mb: { xs: 3, sm: 4 }, maxWidth: "800px", - margin: "0 auto", + mx: "auto", color: theme.palette.text.primary, + fontSize: { xs: "1rem", sm: "1.12rem" }, + lineHeight: 1.75, }}> WAT.ai is a student-run Artificial Intelligence (AI) Organization at the University of Waterloo and the undergraduate student body of the{" "} @@ -145,8 +259,10 @@ const HomePage: React.FC = memo(() => { textAlign: "center", mb: { xs: 4, sm: 5 }, maxWidth: "700px", - margin: "0 auto", + mx: "auto", color: theme.palette.text.secondary, + fontSize: { xs: "0.96rem", sm: "1.05rem" }, + lineHeight: 1.75, }}> Our goal is to establish an environment to enable the continued growth of AI talent and suitable access to opportunities within the @@ -158,30 +274,73 @@ const HomePage: React.FC = memo(() => { variant="outlined" size="large" to="/team" - endIcon={} + endIcon={} > Meet The Team - + {/* Stats Section */} - - + + Our Impact - + Join a thriving community of AI enthusiasts making real impact through research, collaboration, and innovation. - + + {stats.map((stat, index) => ( + + + + + + {stat.label} + + + {stat.description} + + + ))} + @@ -193,20 +352,27 @@ const HomePage: React.FC = memo(() => { padding={8} > - + Our Partners - + We collaborate with leading companies and organizations to provide our members with real-world AI experience and opportunities. @@ -215,20 +381,36 @@ const HomePage: React.FC = memo(() => { {/* CTA Section */} - - + + Ready to Join Us? - + Whether you're a student looking to dive into AI, a professor seeking research collaborations, or a company interested in partnerships, we'd love to work with you. - + *": { + minWidth: { xs: "100%", sm: "170px" }, + fontWeight: "700 !important", + }, + }}> { For Students For Partners From f2cf3f430681db917ad719213457658305aaf08c Mon Sep 17 00:00:00 2001 From: Sharanya Basu Date: Sun, 23 Aug 2026 19:17:05 -0400 Subject: [PATCH 2/4] feat: automate and redesign projects page --- .env.example | 1 + scripts/google-apps-script/Code.gs | 140 ++++++++++ scripts/google-apps-script/README.md | 16 ++ src/components/ModernProjectCard.tsx | 372 ++++++++------------------- src/pages/Projects.tsx | 356 ++++++++----------------- src/services/projectSheet.ts | 251 ++++++++++++++++++ 6 files changed, 624 insertions(+), 512 deletions(-) create mode 100644 .env.example create mode 100644 scripts/google-apps-script/Code.gs create mode 100644 scripts/google-apps-script/README.md create mode 100644 src/services/projectSheet.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1594826 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +REACT_APP_PROJECTS_API_URL= diff --git a/scripts/google-apps-script/Code.gs b/scripts/google-apps-script/Code.gs new file mode 100644 index 0000000..767843e --- /dev/null +++ b/scripts/google-apps-script/Code.gs @@ -0,0 +1,140 @@ +const SPREADSHEET_ID = "1y95UWwpNNwWkoivU2j3jt1q3JwBG-OCpxUjVfW-WV5w"; +const PROJECTS_SHEET_NAME = "Projects for website"; + +function doGet(event) { + const callback = sanitizeCallback_(event && event.parameter && event.parameter.callback); + const payload = JSON.stringify({ + status: "ok", + projects: readProjects_(), + updatedAt: new Date().toISOString(), + }); + + if (callback) { + return ContentService + .createTextOutput(callback + "(" + payload + ");") + .setMimeType(ContentService.MimeType.JAVASCRIPT); + } + + return ContentService + .createTextOutput(payload) + .setMimeType(ContentService.MimeType.JSON); +} + +function readProjects_() { + const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(PROJECTS_SHEET_NAME); + if (!sheet) throw new Error("Missing sheet: " + PROJECTS_SHEET_NAME); + + const range = sheet.getDataRange(); + const values = range.getDisplayValues(); + const richText = range.getRichTextValues(); + if (values.length < 2) return []; + + const headers = values[0].map(normalizeHeader_); + const indexes = { + started: findColumn_(headers, ["started"]), + title: findColumn_(headers, ["project title"]), + summary: findColumn_(headers, ["project summary"]), + leads: findColumn_(headers, ["project leads"]), + members: findColumn_(headers, ["project members"]), + partnership: findColumn_(headers, ["partnerships", "partnership"]), + technology: findColumn_(headers, ["technology"]), + theme: findColumn_(headers, ["theme"]), + result: findColumn_(headers, ["links and results"]), + media: findColumn_(headers, ["demo media", "demo video", "demo image", "media"]), + }; + + return values.slice(1).map(function(row, rowOffset) { + const summary = valueAt_(row, indexes.summary); + const title = valueAt_(row, indexes.title); + if (!title || !summary) return null; + + return { + started: valueAt_(row, indexes.started), + title: title, + summary: summary, + leads: extractLinkedPeople_( + valueAt_(row, indexes.leads), + richText[rowOffset + 1] && richText[rowOffset + 1][indexes.leads] + ), + members: splitList_(valueAt_(row, indexes.members)), + partnership: normalizePartnership_(valueAt_(row, indexes.partnership)), + technologies: splitList_(valueAt_(row, indexes.technology)), + themes: splitList_(valueAt_(row, indexes.theme)), + result: valueAt_(row, indexes.result), + resultUrl: firstLink_(richText[rowOffset + 1] && richText[rowOffset + 1][indexes.result]), + mediaUrl: firstLink_(richText[rowOffset + 1] && richText[rowOffset + 1][indexes.media]) || valueAt_(row, indexes.media), + }; + }).filter(Boolean); +} + +function extractLinkedPeople_(text, richValue) { + const people = splitList_(text); + const linkedRuns = richValue ? richValue.getRuns().map(function(run) { + return { text: clean_(run.getText()), url: run.getLinkUrl() || "" }; + }).filter(function(run) { return run.url; }) : []; + + return people.map(function(name) { + const match = linkedRuns.find(function(run) { + return name.indexOf(run.text) !== -1 || run.text.indexOf(name) !== -1; + }); + return { name: name, linkedin: match ? match.url : "" }; + }); +} + +function firstLink_(richValue) { + if (!richValue) return ""; + const directLink = richValue.getLinkUrl(); + if (directLink) return directLink; + const linkedRun = richValue.getRuns().find(function(run) { return run.getLinkUrl(); }); + return linkedRun ? linkedRun.getLinkUrl() : ""; +} + +function splitList_(value) { + const source = clean_(value).replace(/\s+and\s+/gi, ", "); + const items = []; + let current = ""; + let depth = 0; + + for (let index = 0; index < source.length; index += 1) { + const character = source[index]; + if (character === "(") depth += 1; + if (character === ")") depth = Math.max(0, depth - 1); + if (character === "," && depth === 0) { + if (clean_(current)) items.push(clean_(current)); + current = ""; + } else { + current += character; + } + } + + if (clean_(current)) items.push(clean_(current)); + return items; +} + +function findColumn_(headers, names) { + return headers.findIndex(function(header) { + return names.some(function(name) { return header === name || header.indexOf(name) !== -1; }); + }); +} + +function valueAt_(row, index) { + return index >= 0 ? clean_(row[index]) : ""; +} + +function normalizeHeader_(value) { + return clean_(value).toLowerCase(); +} + +function normalizePartnership_(value) { + const normalized = clean_(value); + return /^(none|n\/a|-)?$/i.test(normalized) ? "" : normalized; +} + +function clean_(value) { + return String(value || "").replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim(); +} + +function sanitizeCallback_(value) { + const callback = String(value || ""); + return /^[A-Za-z_$][0-9A-Za-z_$]*$/.test(callback) ? callback : ""; +} diff --git a/scripts/google-apps-script/README.md b/scripts/google-apps-script/README.md new file mode 100644 index 0000000..a215cde --- /dev/null +++ b/scripts/google-apps-script/README.md @@ -0,0 +1,16 @@ +# Projects Google Apps Script + +This read-only web app exposes the **Projects for website** tab as JSON/JSONP while preserving hyperlinks embedded in rich-text cells. + +1. Open the project spreadsheet. +2. Select **Extensions → Apps Script**. +3. Replace the editor contents with `Code.gs` from this folder. +4. Select **Deploy → New deployment → Web app**. +5. Set **Execute as** to **Me**. +6. Set **Who has access** to **Anyone**. +7. Deploy and copy the `/exec` URL. +8. Add the URL to the website environment as: + + `REACT_APP_PROJECTS_API_URL=https://script.google.com/macros/s/DEPLOYMENT_ID/exec` + +The website automatically falls back to the public Google Sheets endpoint when this variable is absent. diff --git a/src/components/ModernProjectCard.tsx b/src/components/ModernProjectCard.tsx index d0ee324..af91059 100644 --- a/src/components/ModernProjectCard.tsx +++ b/src/components/ModernProjectCard.tsx @@ -1,281 +1,119 @@ -// ModernProjectCard.tsx - Card for displaying project info -// -------------------------------------------------------- -// This component displays a modern card for project details, including title, description, and tags. -// Edit this file to customize card layout, style, or props. - -// Project card for modern projects. Displays title, team, links, and tags. -// To add new fields, update the ModernProjectCardProps interface and usage. -// For design changes, edit the Card and Box props. -import React from "react"; -import { - Box, - Card, - CardContent, - IconButton, - Typography, - useTheme, - Link, - Stack, - Tooltip, - Chip, - Button, -} from "@mui/material"; +import React, { useState } from "react"; +import { Box, Button, Chip, Dialog, DialogContent, DialogTitle, IconButton, Link, Stack, Tooltip, Typography, useTheme } from "@mui/material"; +import { ArrowOutwardRounded, CloseRounded, GroupsRounded, HandshakeRounded, LinkedIn } from "@mui/icons-material"; import { motion } from "framer-motion"; -import { - LinkedIn, - Email, - GitHub, - Article, - Language, - Description, -} from "@mui/icons-material"; -import { TeamMember, ProjectLinks } from "../data/projectData"; +import { SheetProject } from "../services/projectSheet"; -interface ModernProjectCardProps { - title: string; - tpms: TeamMember[]; - description: string; - links?: ProjectLinks; - collaboration?: string; -} +const getYoutubeEmbedUrl = (url?: string) => { + if (!url) return undefined; + const match = url.match(/(?:youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)([^?&/]+)/i); + return match ? `https://www.youtube.com/embed/${match[1]}` : undefined; +}; -const ModernProjectCard: React.FC = ({ - title, - tpms, - description, - links, - collaboration, -}) => { +const GridDetails: React.FC<{ label: string; value: React.ReactNode; icon?: React.ReactNode }> = ({ label, value, icon }) => { const theme = useTheme(); - - // Function to get the appropriate link icon based on the type - const getLinkIcon = (type: 'website' | 'repository' | 'paper' | 'documentation') => { - switch (type) { - case 'website': - return ; - case 'repository': - return ; - case 'paper': - return
; - case 'documentation': - return ; - default: - return ; - } - }; - return ( - - - - {/* Header */} - - - {title} - + + + {icon} + {label} + + {value} + + ); +}; - {/* Collaboration Badge */} - {collaboration && ( - +const ModernProjectCard: React.FC = ({ + title, term, summary, leads, members, partnership, technologies, + themes, result, resultUrl, mediaUrl, +}) => { + const theme = useTheme(); + const [open, setOpen] = useState(false); + const youtubeEmbedUrl = getYoutubeEmbedUrl(mediaUrl); + const isVideoFile = Boolean(mediaUrl?.match(/\.(mp4|webm|ogg)(?:\?|$)/i)); + const inProgress = /^in progress$/i.test(result.trim()); + const completedColor = "#66FF99"; + const leadList = ( + + {leads.map((lead, index) => ( + + {lead.name} + {lead.linkedin && ( + + + + + )} - + + ))} + + ); + + return ( + + + {mediaUrl && ( + + {youtubeEmbedUrl ? ( + + ) : isVideoFile ? ( + + ) : ( + + )} + + )} - {/* Description */} - - {description} - + + + {term || "Project"} + {result && {inProgress ? "In progress" : "Completed"}} + - {/* Bottom Section - pushed to bottom */} - - {/* Project Links */} - {links && Object.keys(links).length > 0 && ( - - - - - {Object.entries(links).map(([key, url]) => ( - - ))} - - - )} + + {title} + + {summary} - {/* TPM Contact Info - With Labels */} - {tpms.length > 0 && ( - - - Technical Project Managers - - - {tpms.map((member, index) => ( - - - {member.name} - - {(member.email || member.linkedin) && ( - - {member.email && ( - - - - - - )} - {member.linkedin && ( - - - - - - )} - - )} - - ))} - - - )} + + + {technologies.map((tag) => )} + {themes.map((tag) => )} + + + Led by{leadList} + + + - - - + + + setOpen(false)} fullWidth maxWidth="md" PaperProps={{ sx: { color: theme.palette.text.primary, backgroundColor: "#131313", backgroundImage: `linear-gradient(145deg, ${theme.palette.primary.main}0B, transparent 38%)`, border: `1px solid ${theme.palette.primary.main}45`, borderRadius: 4, maxHeight: "88vh" } }}> + + + {title} + setOpen(false)} aria-label="Close project" sx={{ position: "absolute", top: 18, right: 18, color: theme.palette.text.secondary }}> + + + + {technologies.map((tag) => )} + {themes.map((tag) => )} + + {summary} + + {members.length > 0 && } + {partnership && } />} + {result && !inProgress && Links and results{result}} + + + ); }; diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx index 81f650a..5685758 100644 --- a/src/pages/Projects.tsx +++ b/src/pages/Projects.tsx @@ -1,273 +1,139 @@ -/** - * Projects - Display active and completed WAT.ai projects - * Includes project cards, stats, and expandable past projects section - */ -import React, { useState, useMemo } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { + Alert, Box, - Typography, + Button, + Chip, + CircularProgress, Container, Grid, - useTheme, - Zoom, + IconButton, + InputAdornment, + MenuItem, Stack, - Paper, TextField, - InputAdornment, - Button, - IconButton, + Typography, + useTheme, } from "@mui/material"; -import { - School, - TrendingUp, - Science, - Search, - Clear, -} from "@mui/icons-material"; -import { ProjectsData } from "../data/projectData"; +import { ClearRounded, RefreshRounded, School, Science, SearchRounded, TrendingUp } from "@mui/icons-material"; import ModernProjectCard from "../components/ModernProjectCard"; +import { loadProjectsFromSheet, SheetProject } from "../services/projectSheet"; + +const CACHE_KEY = "watai-projects-sheet-cache-v1"; -// Projects page: Lists current and past projects. -// Add new projects in ProjectsData array. -// Adjust layout, cards, or stats as needed for your use case. const Projects: React.FC = () => { const theme = useTheme(); - const [searchQuery, setSearchQuery] = useState(""); + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [reloadToken, setReloadToken] = useState(0); + const [search, setSearch] = useState(""); + const [technology, setTechnology] = useState("All technologies"); + const [themeFilter, setThemeFilter] = useState("All themes"); + const [resultFilter, setResultFilter] = useState<"All" | "In progress" | "Results">("All"); - // All projects in one array - const allProjects = useMemo(() => ProjectsData, []); + useEffect(() => { + let active = true; + setLoading(true); + setError(""); - // Filter projects based on search query only - const filteredProjects = useMemo(() => { - return allProjects.filter((project) => { - // Search filter - search by title, description, and TPM names - const tpmNames = project.tpms.map(tpm => tpm.name).join(' '); - const matchesSearch = - searchQuery === "" || - project.title.toLowerCase().includes(searchQuery.toLowerCase()) || - project.description.toLowerCase().includes(searchQuery.toLowerCase()) || - tpmNames.toLowerCase().includes(searchQuery.toLowerCase()); + loadProjectsFromSheet() + .then((data) => { + if (!active) return; + setProjects(data); + localStorage.setItem(CACHE_KEY, JSON.stringify(data)); + }) + .catch((loadError: Error) => { + if (!active) return; + const cached = localStorage.getItem(CACHE_KEY); + if (cached) { + setProjects(JSON.parse(cached)); + setError("Showing the most recently saved project data because the live sheet is temporarily unavailable."); + } else { + setError(loadError.message); + } + }) + .finally(() => active && setLoading(false)); - return matchesSearch; + return () => { active = false; }; + }, [reloadToken]); + + const technologies = useMemo(() => Array.from(new Set(projects.flatMap((project) => project.technologies))).sort(), [projects]); + const themes = useMemo(() => Array.from(new Set(projects.flatMap((project) => project.themes))).sort(), [projects]); + + const filteredProjects = useMemo(() => { + const query = search.trim().toLowerCase(); + return projects.filter((project) => { + const searchable = [project.title, project.summary, project.partnership, ...project.technologies, ...project.themes, ...project.leads.map((lead) => lead.name)].join(" ").toLowerCase(); + const matchesResult = resultFilter === "All" || (resultFilter === "In progress" ? /^in progress$/i.test(project.result) : Boolean(project.result) && !/^in progress$/i.test(project.result)); + return (!query || searchable.includes(query)) + && (technology === "All technologies" || project.technologies.includes(technology)) + && (themeFilter === "All themes" || project.themes.includes(themeFilter)) + && matchesResult; }); - }, [searchQuery, allProjects]); + }, [projects, search, technology, themeFilter, resultFilter]); - const handleClearFilters = () => { - setSearchQuery(""); + const clearFilters = () => { + setSearch(""); + setTechnology("All technologies"); + setThemeFilter("All themes"); + setResultFilter("All"); }; - const heroStats = [ - { icon: , number: allProjects.length.toString(), label: "Total Projects" }, - { icon: , number: allProjects.filter(p => p.active).length.toString(), label: "Active Projects" }, - { icon: , number: (allProjects.length - allProjects.filter(p => p.active).length).toString(), label: "Past Projects" }, - ]; + const resultCount = projects.filter((project) => project.result && !/^in progress$/i.test(project.result)).length; + const inProgressCount = projects.filter((project) => /^in progress$/i.test(project.result)).length; return ( - - {/* Hero Section */} - - - - Our Research Projects - - - Explore our current and past research projects pushing the boundaries of artificial intelligence through innovative research - and cutting-edge applications that make a real-world impact. - - - {/* Stats */} - - {heroStats.map((stat, index) => ( - - - {stat.icon} - - - {stat.number} - - - {stat.label} - - + + + + Research · Engineering · Impact + Our Research Projects + Explore our current and past research projects pushing the boundaries of artificial intelligence through innovative research and cutting-edge applications that make a real-world impact. + }> + {[ + { value: projects.length, label: "Total Projects", icon: }, + { value: inProgressCount, label: "Active Projects", icon: }, + { value: resultCount, label: "Past Projects", icon: }, + ].map((stat) => ( + {stat.icon}{loading ? "–" : stat.value}{stat.label} ))} - - - {/* Search and Filter Section */} - - {/* Search Bar */} - setSearchQuery(e.target.value)} - InputProps={{ - startAdornment: ( - - - - ), - endAdornment: searchQuery && ( - - setSearchQuery("")} - edge="end" - sx={{ p: { xs: 0.5, sm: 1 } }} - > - - - - ), - }} - sx={{ - "& .MuiOutlinedInput-root": { - borderRadius: 2, - fontSize: { xs: "0.9rem", sm: "1rem" }, - padding: { xs: "8px 12px", sm: "10px 14px" }, - "& fieldset": { - borderColor: theme.palette.primary.main, - }, - "&:hover fieldset": { - borderColor: theme.palette.primary.main, - }, - "&.Mui-focused fieldset": { - borderColor: theme.palette.primary.main, - borderWidth: "1px", - }, - }, - "& .MuiOutlinedInput-input": { - padding: { xs: "8px 0", sm: "12px 0" }, - }, - }} - /> - - {/* Current Projects Section */} - - - {filteredProjects.length > 0 ? ( - filteredProjects.map((project, index) => ( - - - - - - - - )) - ) : ( - - - - No projects found - - - Try a different search term - - - + + + + setSearch(event.target.value)} placeholder="Search projects, leads, technology..." InputProps={{ startAdornment: , endAdornment: search ? setSearch("")}> : undefined }} sx={{ "& .MuiOutlinedInput-root": { backgroundColor: "rgba(255,255,255,0.025)", borderRadius: 2.5 } }} /> - )} - + + setTechnology(event.target.value)} label="Technology"> + All technologies{technologies.map((item) => {item})} + + + + setThemeFilter(event.target.value)} label="Theme"> + All themes{themes.map((item) => {item})} + + + + + {(["All", "In progress", "Results"] as const).map((filter) => setResultFilter(filter)} sx={{ color: resultFilter === filter ? "#111" : theme.palette.text.secondary, backgroundColor: resultFilter === filter ? theme.palette.primary.main : "rgba(255,255,255,0.05)", fontWeight: 700 }} />)} + {(search || technology !== "All technologies" || themeFilter !== "All themes" || resultFilter !== "All") && } + + + + {error && } onClick={() => setReloadToken((value) => value + 1)}>Retry} sx={{ mb: 4 }}>{error}} + + {loading ? ( + + ) : filteredProjects.length ? ( + + {filteredProjects.map((project) => )} + + ) : ( + No matching projectsTry clearing a filter or using a broader search. + )} ); diff --git a/src/services/projectSheet.ts b/src/services/projectSheet.ts new file mode 100644 index 0000000..116472d --- /dev/null +++ b/src/services/projectSheet.ts @@ -0,0 +1,251 @@ +export const PROJECT_SHEET_ID = "1y95UWwpNNwWkoivU2j3jt1q3JwBG-OCpxUjVfW-WV5w"; +export const PROJECT_SHEET_GID = "1142127646"; +const PROJECTS_API_URL = process.env.REACT_APP_PROJECTS_API_URL?.trim() + || "https://script.google.com/macros/s/AKfycbxrq50_YqA2gwj_r-CIECvsVsFZVDeYq1vBfajUJHoaCUEHufunx1qsx2ptC9F__fHv/exec"; + +export interface SheetProjectLead { + name: string; + linkedin?: string; +} + +export interface SheetProject { + id: string; + started: string; + term: string; + year: number; + title: string; + summary: string; + leads: SheetProjectLead[]; + members: string[]; + partnership?: string; + technologies: string[]; + themes: string[]; + result: string; + resultUrl?: string; + mediaUrl?: string; +} + +interface GvizCell { + v?: string | number | null; + f?: string; +} + +interface GvizResponse { + status: string; + errors?: Array<{ message?: string }>; + table?: { + cols: Array<{ label?: string }>; + rows: Array<{ c: Array }>; + }; +} + +interface AppsScriptResponse { + status: string; + projects: Array<{ + started?: string; + title?: string; + summary?: string; + leads?: SheetProjectLead[]; + members?: string[]; + partnership?: string; + technologies?: string[]; + themes?: string[]; + result?: string; + resultUrl?: string; + mediaUrl?: string; + }>; +} + +let requestSequence = 0; + +const clean = (value: unknown) => + String(value ?? "") + .replace(/\u00a0/g, " ") + .replace(/\s+/g, " ") + .trim(); + +const splitList = (value: string) => { + const source = clean(value).replace(/\s+and\s+/gi, ", "); + const items: string[] = []; + let current = ""; + let parenthesesDepth = 0; + + for (const character of source) { + if (character === "(") parenthesesDepth += 1; + if (character === ")") parenthesesDepth = Math.max(0, parenthesesDepth - 1); + + if ((character === "," || character === "\n") && parenthesesDepth === 0) { + if (clean(current)) items.push(clean(current)); + current = ""; + } else { + current += character; + } + } + + if (clean(current)) items.push(clean(current)); + return items; +}; + +const normalizePartnership = (value: string) => { + const normalized = clean(value); + return /^(none|n\/a|-)?$/i.test(normalized) ? undefined : normalized; +}; + +const getTerm = (started: string) => { + const normalized = clean(started); + const year = Number(normalized.match(/\b(20\d{2})\b/)?.[1] || 0); + const month = normalized.toLowerCase(); + const season = month.startsWith("jan") + ? "Winter" + : month.startsWith("may") + ? "Spring" + : month.startsWith("sep") + ? "Fall" + : normalized.replace(/\s*20\d{2}.*/, ""); + + return { term: [season, year || ""].filter(Boolean).join(" "), year }; +}; + +const slugify = (value: string) => + value + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, ""); + +const findUrl = (value: string) => value.match(/https?:\/\/[^\s,]+/i)?.[0]; + +const mapResponse = (response: GvizResponse): SheetProject[] => { + if (response.status !== "ok" || !response.table) { + throw new Error(response.errors?.[0]?.message || "Google Sheets returned an invalid response."); + } + + const headers = response.table.cols.map((column) => clean(column.label).toLowerCase()); + const columnIndex = (...names: string[]) => + headers.findIndex((header) => names.some((name) => header === name || header.includes(name))); + + const indexes = { + started: columnIndex("started"), + title: columnIndex("project title"), + summary: columnIndex("project summary"), + leads: columnIndex("project leads"), + members: columnIndex("project members"), + partnership: columnIndex("partnership"), + technology: columnIndex("technology"), + theme: columnIndex("theme"), + result: columnIndex("links and results"), + media: columnIndex("demo media", "demo video", "demo image", "media"), + }; + + const cellValue = (cells: Array, index: number) => { + if (index < 0) return ""; + const cell = cells[index]; + return clean(cell?.f ?? cell?.v ?? ""); + }; + + return response.table.rows + .map((row) => { + const started = cellValue(row.c, indexes.started); + const title = cellValue(row.c, indexes.title); + const summary = cellValue(row.c, indexes.summary); + const result = cellValue(row.c, indexes.result); + const mediaUrl = findUrl(cellValue(row.c, indexes.media)); + const { term, year } = getTerm(started); + + return { + id: `${slugify(title)}-${slugify(term)}`, + started, + term, + year, + title, + summary, + leads: splitList(cellValue(row.c, indexes.leads)).map((name) => ({ name })), + members: splitList(cellValue(row.c, indexes.members)), + partnership: normalizePartnership(cellValue(row.c, indexes.partnership)), + technologies: splitList(cellValue(row.c, indexes.technology)), + themes: splitList(cellValue(row.c, indexes.theme)), + result, + resultUrl: findUrl(result), + mediaUrl, + }; + }) + .filter((project) => project.title && project.summary) + .sort((a, b) => b.year - a.year || b.started.localeCompare(a.started)); +}; + +const mapAppsScriptResponse = (response: AppsScriptResponse): SheetProject[] => { + if (response.status !== "ok" || !Array.isArray(response.projects)) { + throw new Error("The projects API returned an invalid response."); + } + + return response.projects + .map((project) => { + const started = clean(project.started); + const title = clean(project.title); + const summary = clean(project.summary); + const result = clean(project.result); + const { term, year } = getTerm(started); + return { + id: `${slugify(title)}-${slugify(term)}`, + started, + term, + year, + title, + summary, + leads: (project.leads || []).map((lead) => ({ name: clean(lead.name), linkedin: clean(lead.linkedin) || undefined })).filter((lead) => lead.name), + members: (project.members || []).map(clean).filter(Boolean), + partnership: normalizePartnership(clean(project.partnership)), + technologies: (project.technologies || []).map(clean).filter(Boolean), + themes: (project.themes || []).map(clean).filter(Boolean), + result, + resultUrl: clean(project.resultUrl) || findUrl(result), + mediaUrl: findUrl(clean(project.mediaUrl)), + }; + }) + .filter((project) => project.title && project.summary) + .sort((a, b) => b.year - a.year || b.started.localeCompare(a.started)); +}; + +export const loadProjectsFromSheet = () => + new Promise((resolve, reject) => { + const callbackName = `__wataiProjectsCallback_${Date.now()}_${requestSequence++}`; + const callbackHost = window as unknown as Record void) | undefined>; + const script = document.createElement("script"); + const timeout = window.setTimeout(() => { + cleanup(); + reject(new Error("The project sheet took too long to respond.")); + }, 12000); + + const cleanup = () => { + window.clearTimeout(timeout); + delete callbackHost[callbackName]; + script.remove(); + }; + + callbackHost[callbackName] = (response) => { + try { + resolve("projects" in response ? mapAppsScriptResponse(response) : mapResponse(response)); + } catch (error) { + reject(error); + } finally { + cleanup(); + } + }; + + script.onerror = () => { + cleanup(); + reject(new Error("Unable to load projects from Google Sheets.")); + }; + + if (PROJECTS_API_URL) { + const separator = PROJECTS_API_URL.includes("?") ? "&" : "?"; + script.src = `${PROJECTS_API_URL}${separator}callback=${encodeURIComponent(callbackName)}`; + } else { + const query = new URLSearchParams({ + gid: PROJECT_SHEET_GID, + tqx: `responseHandler:${callbackName}`, + headers: "1", + }); + script.src = `https://docs.google.com/spreadsheets/d/${PROJECT_SHEET_ID}/gviz/tq?${query}`; + } + document.head.appendChild(script); + }); From 93b4187508d922fdad68e20f7791f3b8986ae5e6 Mon Sep 17 00:00:00 2001 From: Sharanya Basu Date: Tue, 15 Sep 2026 21:13:23 -0400 Subject: [PATCH 3/4] Automate project imports with version-controlled JSON --- docs/projects-workflow.md | 15 + package.json | 1 + scripts/google-apps-script/Code.gs | 2 + scripts/google-apps-script/README.md | 8 +- scripts/import-projects.js | 99 +++ src/components/ModernProjectCard.tsx | 34 +- src/data/projectData.ts | 425 ------------- src/data/projects.json | 914 +++++++++++++++++++++++++++ src/pages/Projects.tsx | 52 +- src/services/projectData.ts | 4 + src/services/projectSheet.ts | 251 -------- src/types/project.ts | 21 + 12 files changed, 1092 insertions(+), 734 deletions(-) create mode 100644 docs/projects-workflow.md create mode 100644 scripts/import-projects.js delete mode 100644 src/data/projectData.ts create mode 100644 src/data/projects.json create mode 100644 src/services/projectData.ts delete mode 100644 src/services/projectSheet.ts create mode 100644 src/types/project.ts diff --git a/docs/projects-workflow.md b/docs/projects-workflow.md new file mode 100644 index 0000000..2645884 --- /dev/null +++ b/docs/projects-workflow.md @@ -0,0 +1,15 @@ +# Projects publishing workflow + +The committed file `src/data/projects.json` is the website's only project-data source. Editing the spreadsheet does not change the live website. + +## Publish spreadsheet changes + +1. Edit the **Projects for website** sheet. +2. Give every project a permanent, unique **Project ID** such as `chefost`. Never reuse or rename an ID. +3. Run `npm run import-projects` from the repository root. +4. Review the diff in `src/data/projects.json`. +5. Commit and push the JSON change through the normal review process. + +The importer updates records with matching IDs and adds new records. It deliberately preserves JSON records that are absent from the spreadsheet, so historical projects cannot be deleted accidentally by removing a row. Duplicate IDs, missing titles, and missing summaries stop the import without changing the JSON file. + +The Apps Script is read-only. It must be run manually through the import command; spreadsheet edits never trigger a deployment or website update. diff --git a/package.json b/package.json index 7ba35e6..a16171f 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "optimize:images": "node scripts/optimize-images.js", "optimize:images:clean": "node scripts/optimize-images.js --clean", "optimize:images:help": "node scripts/optimize-images.js --help", + "import-projects": "node scripts/import-projects.js", "test": "react-scripts test", "eject": "react-scripts eject", "predeploy": "npm run build", diff --git a/scripts/google-apps-script/Code.gs b/scripts/google-apps-script/Code.gs index 767843e..b1baf61 100644 --- a/scripts/google-apps-script/Code.gs +++ b/scripts/google-apps-script/Code.gs @@ -31,6 +31,7 @@ function readProjects_() { const headers = values[0].map(normalizeHeader_); const indexes = { + id: findColumn_(headers, ["project id"]), started: findColumn_(headers, ["started"]), title: findColumn_(headers, ["project title"]), summary: findColumn_(headers, ["project summary"]), @@ -49,6 +50,7 @@ function readProjects_() { if (!title || !summary) return null; return { + projectId: valueAt_(row, indexes.id), started: valueAt_(row, indexes.started), title: title, summary: summary, diff --git a/scripts/google-apps-script/README.md b/scripts/google-apps-script/README.md index a215cde..29a1189 100644 --- a/scripts/google-apps-script/README.md +++ b/scripts/google-apps-script/README.md @@ -1,6 +1,6 @@ # Projects Google Apps Script -This read-only web app exposes the **Projects for website** tab as JSON/JSONP while preserving hyperlinks embedded in rich-text cells. +This read-only web app exposes the **Projects for website** tab as JSON while preserving hyperlinks embedded in rich-text cells. The website does not read it at runtime; `npm run import-projects` uses it to update the version-controlled dataset. 1. Open the project spreadsheet. 2. Select **Extensions → Apps Script**. @@ -9,8 +9,8 @@ This read-only web app exposes the **Projects for website** tab as JSON/JSONP wh 5. Set **Execute as** to **Me**. 6. Set **Who has access** to **Anyone**. 7. Deploy and copy the `/exec` URL. -8. Add the URL to the website environment as: +8. Optionally set the URL when running the importer: - `REACT_APP_PROJECTS_API_URL=https://script.google.com/macros/s/DEPLOYMENT_ID/exec` + `PROJECTS_API_URL=https://script.google.com/macros/s/DEPLOYMENT_ID/exec npm run import-projects` -The website automatically falls back to the public Google Sheets endpoint when this variable is absent. +Add a permanent, unique **Project ID** column to the sheet. Use lowercase identifiers such as `chefost`; never change an ID after publishing a project. Run `npm run import-projects`, review `src/data/projects.json`, then commit it. Projects missing from the sheet are preserved rather than deleted. diff --git a/scripts/import-projects.js b/scripts/import-projects.js new file mode 100644 index 0000000..f3348b5 --- /dev/null +++ b/scripts/import-projects.js @@ -0,0 +1,99 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +const DEFAULT_API_URL = "https://script.google.com/macros/s/AKfycbxrq50_YqA2gwj_r-CIECvsVsFZVDeYq1vBfajUJHoaCUEHufunx1qsx2ptC9F__fHv/exec"; +const API_URL = String(process.env.PROJECTS_API_URL || DEFAULT_API_URL).trim(); +const DATA_PATH = path.resolve(__dirname, "../src/data/projects.json"); + +const clean = (value) => String(value ?? "").replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim(); +const slugify = (value) => clean(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""); +const firstUrl = (value) => clean(value).match(/https?:\/\/[^\s,]+/i)?.[0]; +const normalizeList = (value) => (Array.isArray(value) ? value : []).map(clean).filter(Boolean); + +const getTerm = (started) => { + const normalized = clean(started); + const year = Number(normalized.match(/\b(20\d{2})\b/)?.[1] || 0); + const lower = normalized.toLowerCase(); + const season = lower.startsWith("jan") ? "Winter" : lower.startsWith("may") ? "Summer" : lower.startsWith("sep") ? "Fall" : normalized.replace(/\s*20\d{2}.*/, ""); + return { term: [season, year || ""].filter(Boolean).join(" "), year }; +}; + +const comparable = (value) => clean(value).toLowerCase(); + +const normalizeProject = (source) => { + const started = clean(source.started); + const title = clean(source.title); + const summary = clean(source.summary); + if (!title || !summary) throw new Error(`Every imported project needs a title and summary (received "${title || "untitled"}").`); + + const { term, year } = getTerm(started); + const rawId = clean(source.projectId || source.id || ""); + const id = slugify(rawId); + if (!rawId) throw new Error(`Project "${title}" is missing its permanent Project ID.`); + if (id !== rawId) throw new Error(`Project ID "${rawId}" must contain only lowercase letters, numbers, and hyphens.`); + + const result = clean(source.result); + return { + id, + started, + term, + year, + title, + summary, + leads: (Array.isArray(source.leads) ? source.leads : []).map((lead) => ({ + name: clean(lead?.name), + ...(clean(lead?.linkedin) ? { linkedin: clean(lead.linkedin) } : {}), + })).filter((lead) => lead.name), + members: normalizeList(source.members), + ...(clean(source.partnership) ? { partnership: clean(source.partnership) } : {}), + technologies: normalizeList(source.technologies), + themes: normalizeList(source.themes), + result, + ...(clean(source.resultUrl) || firstUrl(result) ? { resultUrl: clean(source.resultUrl) || firstUrl(result) } : {}), + ...(firstUrl(source.mediaUrl) ? { mediaUrl: firstUrl(source.mediaUrl) } : {}), + }; +}; + +const validateUniqueIds = (projects, label) => { + const seen = new Set(); + for (const project of projects) { + if (seen.has(project.id)) throw new Error(`Duplicate project ID "${project.id}" in ${label}.`); + seen.add(project.id); + } +}; + +async function main() { + const existing = JSON.parse(fs.readFileSync(DATA_PATH, "utf8")); + if (!Array.isArray(existing)) throw new Error("projects.json must contain an array."); + validateUniqueIds(existing, "projects.json"); + + const response = await fetch(API_URL); + if (!response.ok) throw new Error(`Projects API returned HTTP ${response.status}.`); + const payload = await response.json(); + if (payload.status !== "ok" || !Array.isArray(payload.projects)) throw new Error("Projects API returned an invalid response."); + + const incoming = payload.projects.map(normalizeProject); + validateUniqueIds(incoming, "spreadsheet import"); + + const representsSameProject = (left, right) => left.id === right.id || ( + comparable(left.title) === comparable(right.title) + && comparable(left.started) === comparable(right.started) + ); + const merged = existing.filter((project) => !incoming.some((candidate) => representsSameProject(project, candidate))); + merged.push(...incoming); + validateUniqueIds(merged, "merged projects"); + merged.sort((a, b) => (b.year || 0) - (a.year || 0) || clean(b.started).localeCompare(clean(a.started)) || a.title.localeCompare(b.title)); + + const temporaryPath = `${DATA_PATH}.tmp`; + fs.writeFileSync(temporaryPath, `${JSON.stringify(merged, null, 2)}\n`); + fs.renameSync(temporaryPath, DATA_PATH); + console.log(`Imported ${incoming.length} spreadsheet projects; projects.json now contains ${merged.length} projects.`); + console.log("Review the git diff before committing."); +} + +main().catch((error) => { + console.error(`Project import failed: ${error.message}`); + process.exitCode = 1; +}); diff --git a/src/components/ModernProjectCard.tsx b/src/components/ModernProjectCard.tsx index af91059..73645ab 100644 --- a/src/components/ModernProjectCard.tsx +++ b/src/components/ModernProjectCard.tsx @@ -1,13 +1,13 @@ import React, { useState } from "react"; import { Box, Button, Chip, Dialog, DialogContent, DialogTitle, IconButton, Link, Stack, Tooltip, Typography, useTheme } from "@mui/material"; -import { ArrowOutwardRounded, CloseRounded, GroupsRounded, HandshakeRounded, LinkedIn } from "@mui/icons-material"; +import { ArrowOutwardRounded, CloseRounded, GroupsRounded, HandshakeRounded, LinkedIn, PlayCircleOutlineRounded } from "@mui/icons-material"; import { motion } from "framer-motion"; -import { SheetProject } from "../services/projectSheet"; +import { Project } from "../types/project"; -const getYoutubeEmbedUrl = (url?: string) => { +const getYoutubeVideoId = (url?: string) => { if (!url) return undefined; const match = url.match(/(?:youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)([^?&/]+)/i); - return match ? `https://www.youtube.com/embed/${match[1]}` : undefined; + return match?.[1]; }; const GridDetails: React.FC<{ label: string; value: React.ReactNode; icon?: React.ReactNode }> = ({ label, value, icon }) => { @@ -23,13 +23,15 @@ const GridDetails: React.FC<{ label: string; value: React.ReactNode; icon?: Reac ); }; -const ModernProjectCard: React.FC = ({ +const ModernProjectCard: React.FC = ({ title, term, summary, leads, members, partnership, technologies, themes, result, resultUrl, mediaUrl, }) => { const theme = useTheme(); const [open, setOpen] = useState(false); - const youtubeEmbedUrl = getYoutubeEmbedUrl(mediaUrl); + const youtubeVideoId = getYoutubeVideoId(mediaUrl); + const youtubeEmbedUrl = youtubeVideoId ? `https://www.youtube.com/embed/${youtubeVideoId}` : undefined; + const youtubeThumbnailUrl = youtubeVideoId ? `https://i.ytimg.com/vi/${youtubeVideoId}/hqdefault.jpg` : undefined; const isVideoFile = Boolean(mediaUrl?.match(/\.(mp4|webm|ogg)(?:\?|$)/i)); const inProgress = /^in progress$/i.test(result.trim()); const completedColor = "#66FF99"; @@ -61,10 +63,13 @@ const ModernProjectCard: React.FC = ({ }}> {mediaUrl && ( - {youtubeEmbedUrl ? ( - + {youtubeThumbnailUrl ? ( + ) : isVideoFile ? ( - + + + Video demo + ) : ( )} @@ -102,6 +107,17 @@ const ModernProjectCard: React.FC = ({ setOpen(false)} aria-label="Close project" sx={{ position: "absolute", top: 18, right: 18, color: theme.palette.text.secondary }}> + {open && mediaUrl && ( + + {youtubeEmbedUrl ? ( + + ) : isVideoFile ? ( + + ) : ( + + )} + + )} {technologies.map((tag) => )} {themes.map((tag) => )} diff --git a/src/data/projectData.ts b/src/data/projectData.ts deleted file mode 100644 index e440c26..0000000 --- a/src/data/projectData.ts +++ /dev/null @@ -1,425 +0,0 @@ -// projectData.ts - Project data for WAT.ai -// ----------------------------------------------------------- -// This file contains all WAT.ai projects (active and past) for display on the Projects page. -// Edit this file to add, remove, or update project information. - - -export interface TeamMember { - name: string; - email?: string; - linkedin?: string; -} - -export interface ProjectLinks { - website?: string; - repository?: string; - paper?: string; - documentation?: string; -} - -export interface ProjectData { - title: string; - tpm?: string; // Deprecated - for backwards compatibility - tpms: TeamMember[]; - description: string; - links?: ProjectLinks; - collaboration?: string; - active?: boolean; -} - -export const ProjectsData: ProjectData[] = [ - { - title: "Deep Learning Race Car", - description: - "Self-driving technology could prevent millions of traffic deaths caused by human error each year. The Deep Learning Race Car project advances autonomous vehicle research by building a miniature race car that learns to navigate tracks independently. Racing serves as an ideal testing ground for autonomous systems because it demands split-second decisions and rapid adaptation to new environments. These challenges directly translate to real-world self-driving scenarios. By demonstrating how AI can master high-speed navigation in constrained spaces, this project contributes to making autonomous vehicles safer and more reliable for everyone.", - tpms: [ - { - name: "Nina Zhang", - email: "nina123hz@gmail.com", - linkedin: "https://www.linkedin.com/in/nina-zhang-a85935310/" - } - ], - active: true, - }, - { - title: "ClipABit: Semantic Video Search Engine", - description: - "Video editors waste countless hours manually scrubbing through hundreds of footage files to find specific moments for their projects. ClipABit transforms this tedious process into seconds by enabling natural language search across entire video libraries. Simply describe what you're looking for: a sunset scene, a person laughing, or someone walking through a doorway, and instantly locate those exact moments. By making video content as searchable as text, ClipABit empowers creators to focus on storytelling instead of file management, dramatically accelerating the creative process for filmmakers, content creators, and media professionals.", - tpms: [ - { - name: "Eshaan Mehta", - email: "e3mehta@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/eshaan-mehta-136a6924b/" - }, - { - name: "Safiya Makada", - email: "smakada@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/safiya-makada/" - } - ], - links: { - repository: "https://github.com/ClipABit" - }, - active: true, - }, - { - title: "FlockRL: Decentralized Drone Swarm Coordination", - description: - "From disaster response to search and rescue operations, coordinated drone swarms could transform how we handle emergencies and complex tasks. FlockRL tackles the fundamental challenge of enabling drones to work together safely without relying on a central controller. This is crucial for scenarios where communication networks fail or coordination needs to happen faster than any single controller could manage. By teaching drones to navigate obstacle-filled environments while coordinating only with nearby neighbors, this research paves the way for resilient autonomous systems that can adapt to unpredictable real-world conditions, from collapsed buildings to forest fires.", - tpms: [ - { - name: "Joshua Zhang", - email: "jlzhang@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/joshualeezhang/" - }, - { - name: "Katie Zhong", - email: "k2zhong@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/katie-zhong/" - } - ], - active: true, - }, - { - title: "Pianofi: AI-Powered Piano Transcription", - description: - "Musicians spend hours painstakingly transcribing songs by ear or settle for inaccurate auto-generated sheet music. PianoFi democratizes music learning by instantly transforming any song into professional-quality piano sheet music. Whether you're a beginner wanting to learn your favorite pop song or an advanced pianist seeking accurate transcriptions of complex pieces, PianoFi makes quality practice material accessible to everyone. By eliminating the barrier between hearing a song and being able to play it, this tool empowers musicians at all skill levels to learn, practice, and perform the music they love.", - tpms: [ - { - name: "Jonathan Gong", - email: "j56gong@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/jonathan-gong-005491263/" - }, - { - name: "Bruce Wang", - email: "b225wang@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/brucewang15/" - } - ], - links: { - repository: "https://github.com/Pianofi", - website: "https://pianofi.ca" - }, - active: true, - }, - { - title: "See-DR: Mobile Diabetic Retinopathy Screening", - description: - "Diabetic retinopathy is a leading cause of preventable blindness, yet many at-risk patients lack access to regular eye screenings due to the cost and scarcity of specialized equipment. See-DR addresses this healthcare gap by transforming any smartphone into a portable screening device that can detect early signs of vision-threatening eye disease. By making screenings accessible in community clinics, pharmacies, and underserved areas, this tool has the potential to catch diabetic retinopathy before it causes irreversible damage, saving sight and improving quality of life for millions of people with diabetes worldwide.", - tpms: [ - { - name: "Jessica Yuan", - email: "jl2yuan@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/jessica-yuan1/" - }, - { - name: "Hari Chandrasekhar", - email: "hchandra@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/hari-chandrasekhar/" - } - ], - links: { - repository: "https://github.com/jessicayuan1/see-dr" - }, - active: true, - }, - { - title: "Microgrid RL: Autonomous Optimization of Renewable-Powered Microgrids", - description: - "Millions of people in rural Sub-Saharan Africa lack reliable access to electricity, hindering economic development and quality of life. Renewable-powered microgrids offer a solution, but managing the complex balance between solar generation, battery storage, and fluctuating demand remains challenging and costly. This project develops AI systems that autonomously optimize microgrid operations, making clean energy more reliable and affordable for off-grid communities. By reducing operational complexity and maximizing the use of renewable resources, this work directly contributes to expanding energy access in regions that need it most, enabling education, healthcare, and economic opportunities.", - tpms: [ - { - name: "Jordan Leis", - email: "j2leis@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/jordan-leis" - }, - { - name: "Devon Kisob", - email: "dkisob@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/devonkisob/" - } - ], - active: true, - }, - { - title: "AI Sentiment Pulse in Stock Market", - description: - "AI Sentiment Pulse in Stock Market is an application-focused project that leverages social media sentiment to anticipate short-term market trends. Inspired by the 2021 GameStop surge—where Reddit discussions triggered dramatic price shifts—we built a system to monitor real-time updates from subreddits like r/WallStreetBets. Instead of focusing on individual stocks, we analyze trending topics and collective user sentiment to detect sudden shifts in market attention. To improve accuracy, our system includes sarcasm detection tailored to the informal and ironic language often used in financial communities. These insights are combined with market data and processed using classical and deep learning models to forecast potential price movements. Our goal is to equip investors with timely, sentiment-driven signals that highlight emerging retail momentum.", - tpms: [ - { - name: "Jiayou (Sam) Zhong", - email: "j55zhong@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/jiayouz/" - }, - { - name: "Shenyan (Bob) Zheng", - email: "b63zheng@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/shenyan-zheng-0ab064274/" - } - ], - active: true, - }, - { - title: "AffiNNity: Predicting Drug–Target Binding with Graph Neural Networks", - description: - "Drug discovery is slow, expensive, and often hit-or-miss. AffiNNity is a machine learning model designed to change that — using Graph Neural Networks (GNNs) to predict how strongly a drug will bind to a target protein. By combining molecular graphs with protein sequence data in a dual-stream setup, the system captures both the shape and behavior of drug–protein interactions. Built on top of large datasets like PDBBind and modern architectures like Graph Isomorphism Networks, AffiNNity learns the patterns that make a drug effective — without needing to run thousands of costly lab tests. The goal is to speed up early-phase drug screening, reduce experimental overhead, and help scientists identify the most promising candidates faster. As pharmaceutical pipelines increasingly rely on computational tools, AffiNNity offers a scalable, accurate way to bring life-saving treatments to patients more efficiently.", - tpms: [ - { - name: "Jahkim Brown-Roopnarine", - email: "jbrownro@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/jahkim-brown-roopnarine/" - }, - { - name: "James Yu", - email: "j85yu@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/james-yu2005/" - } - ], - active: true, - }, - { - title: "FORTif.ai: AI-Driven Companion for Senior Independence", - description: - "FORTif.ai is an AI-driven companion that empowers seniors to live independently by merging proactive safety monitoring with tailored daily support. Using a computer-vision–powered Hazard Detection model, it continuously scans the home for potential risks—like spills, cluttered pathways, and tripping hazards—and offers clear, actionable recommendations to address them. At the same time, an intuitive AI chatbot engages users in friendly, proactive conversations, providing timely medication and appointment reminders, personalized wellness check-ins, and empathetic responses to questions or concerns. With built-in voice-to-text capabilities and real-time safety insights, FORTif.ai delivers a seamless, user-centric experience designed to enhance home safety, streamline everyday routines, and foster lasting independence for seniors.", - tpms: [ - { - name: "Lino Kee", - email: "lino.kee@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/linokee0423/" - }, - { - name: "Edson Takei", - email: "ektakei@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/edsontakei/" - } - ], - active: true, - }, - { - title: "Oliver: AI-Powered Virtual Teaching Assistant", - description: - "In recent years, advancements in conversational AI have led to the development of intelligent tutoring systems to enhance learning experiences through interactive conversation. This project, Oliver, is an innovative virtual teaching assistant and course management system that leverages contextual memory and response strategies designed to promote active learning and critical thinking. Unlike traditional models that frequently offer direct answers, Oliver encourages exploration and comprehension. Through our research paper, we underscore Oliver's potential to serve as a powerful tool in education, supporting learners in developing deeper cognitive skills rather than relying on rote memorization.", - tpms: [ - { - name: "Haoran Zhu", - linkedin: "https://www.linkedin.com/in/haoran-zhu-5243b0186/" - }, - - ], - links: { - repository: "https://github.com/XiandaDu/WatAIOliver", - paper: "https://ieeexplore.ieee.org/document/10975875/" - }, - active: true, - }, - { - title: "Radiel Health: Personalized Medicine Through Computational Fluid Dynamics", - description: - "Surgical diagnosis has always been very observational, decisions were based off of what could be seen. There have been many successful attempts to add more quantitative metrics in medicine, but still diagnoses tend to be 'one-size-fits-all'. The next big leap in the field comes in the form of personalized medicine, where each patient has their own customized treatment that's shaped through empirical research. We are developing a platform where surgeons can upload ultrasounds of an artery, which is then turned into a 3D mesh and is run through our ML model. This model, trained on geometric and physics-based data from ultrasounds, allows us to quickly and accurately predict critical flow parameters for surgeries like coronary interventions. The goal is to minimize the cost to make these parameters available to surgeons from any clinic, making diagnoses more informed.", - tpms: [ - { - name: "Rishabh Sharma", - email: "r342shar@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/rishabh2003sharma/" - }, - { - name: "Ahash Ganeshamoorthy", - email: "ahash.ganeshamoorthy@griffithuni.edu.au", - linkedin: "https://www.linkedin.com/in/ahash-ganeshamoorthy/" - }, - { - name: "Jatin Mehta", - email: "jatin.mehta@uwaterloo.ca", - linkedin: "https://www.linkedin.com/in/jatin-r-mehta/" - } - ], - links: { - website: "https://radielhealth.com/", - documentation: "https://radielhealth.notion.site/from-barbarism-to-digital-twins" - }, - collaboration: "UW Fluid Flow Physics Group", - active: true, - }, - { - title: "Audio Temporal Segmentation & Sentiment Analysis", - description: - "Our project aims to analyze audio signals to detect phrases and understand emotions. We break down audio into smaller parts (called phrases) and study their patterns over time. This is done using a supervised dataset of audio with labeled phrases and different emotional tags corresponding to each phrase. By looking at the frequency and timing of sounds, we can predict and identify key phrases. We also apply language processing methods to analyze the emotional tone of the audio. This helps improve technologies like phrase detection and language analysis in various multimedia applications.", - tpms: [ - { name: "Krish Patel" }, - { name: "Sahal Sajeer Kalandan" } - ], - }, - { - title: "BCI Signal Decoding for Motor Control", - description: - "We aim to develop a Spiking Neural Network that can translate neural activity from the brain's motor areas into muscle control commands. Using implanted electrode data from Macaques, our SNNs will provide insights for advanced prosthetic control and efficient brain-computer interface implementations.", - tpms: [ - { name: "Jakeb Chouinard" }, - { name: "Raihan A. Vaheed" } - ], - }, - { - title: "Causal Modeling and Time Series Representation Learning for Diabetes Management", - description: - "Gluroo aims to simplify diabetes management by streamlining the tracking of fitness, nutrition, and insulin use for people with diabetes (PWD). This project focuses on improving short-term prandial (meal-time) and postprandial blood glucose outcomes for people with type 1 diabetes, a complex disease that affects nearly 10 million people worldwide. We aim to leverage semi-supervised learning to identify unlabelled meals in time-series blood glucose data, develop meal-scoring functions, and explore causal machine-learning techniques. Our goal is to provide actionable insights to PWD and their care practitioners, enhancing health outcomes and quality of life.", - tpms: [ - { name: "Christopher Risi" }, - { name: "Walker Payne" }, - { name: "Dvir Zagury-Grynbaum" } - ], - collaboration: "Gluroo", - }, - { - title: "Copyright Detection in Large Language Models - An ethical approach to Generative AI Development", - description: - "Use of copyrighted content in training generative AI models has increased significantly as the field has emerged, however, it is challenging to know whether a model is using copyrighted material in its training data. There are two project goals The first is to develop tools to detect whether a piece of given content was used in training data of an LLM. The second is to build an open-source RAG-based logging system to keep track of pieces of flagged media that have been found in the training data of publicly available LLMs.", - tpms: [ - { name: "Senan Gaffori" }, - { name: "Khushee Kapoor" } - ], - }, - { - title: "Deep Reinforcement Learning for Stock Portfolio Optimization", - description: - "We are developing a reinforcement learning policy-agent model that optimizes a stock portfolio for long-term capital gains. This model will trade on stocks, commodities and indexes, and have access to real-time data on individual assets, as well as various indicators. In simpler words, we're making a model to make your Wealthsimple balance go up fast! 💰", - tpms: [ - { name: "Balambika Baskaran" }, - { name: "Ali Elhor" } - ], - }, - { - title: "DelayNoMore: TTC Bus Delay Forecaster", - description: - "The goal of this project is to develop a model that accurately predicts whether a bus route will be delayed by leveraging previous years' TTC delay data which includes time of delay, location, route, vehicle number, and a few other fields. We also plan on expanding these fields by including seasonality, weather/road conditions, and other applicable features.", - tpms: [ - { name: "Ted Ferris" }, - { name: "Chow Sheng Liang" }, - { name: "Franklin Ramirez" } - ], - }, - { - title: "Deploying AI onboard satellite microcontrollers for the Semantic Segmentation of Methane Plumes with Hyperspectral ML Models", - description: - "This project aims to develop and deploy artificial intelligence onboard microcontrollers to identify and segment methane plumes using hyperspectral machine learning models. The goal is to enhance environmental monitoring and contribute to climate change mitigation by providing precise and real-time data on methane emissions. The STARCOP dataset provides a fully annotated dataset to be used for training and evaluation. This project will aim to develop a model on this dataset and benchmark it against existing solutions.", - tpms: [ - { name: "Liam McAlpine" }, - { name: "Prahar Ijner" }, - { name: "Kaxit Pandya" } - ], - }, - { - title: "Energy-efficient AI Accelerators for Transformer Models", - description: - "We'll be creating an AI accelerator (a computer chip optimised to run AI models) for transformers. Our specific goal is to optimise the energy consumption of this AI accelerator, given the rising energy demand from data centres housing computer LLM inference. Our final digital accelerator design will be manufactured using Tiny Tapeout, whereas intermediate testing will occur using industry simulation softwares and field-programmable gate arrays (FPGAs).", - tpms: [ - { name: "Madhav Malhotra" } - ], - }, - { - title: "NuanceEdge", - description: - "Open science is critical for a society's development and prosperity through knowledge sharing, collaboration, public trust, and evidence-informed decision making. It is currently difficult for policymakers to access and understand latest scientific results, making it difficult to appropriately inform decision making with the latest knowledge. Our team will be looking to address this problem by developing a web-based platform, NuanceEdge, aimed at making science more accessible through extraction of key insights and better presentation using generative AI tools.", - tpms: [ - { name: "Rachel Heo" }, - { name: "Ethan Lem" }, - { name: "Shruti Srivatsan" } - ], - collaboration: "W&W", - }, - { - title: "Pitch AI", - description: - "We aim to develop a generative AI tool to create movie trailers for the media industry. We'll be using machine learning and deep learning to generate scenarios. The input parameters for the trailer generation include character ideas, plot points, and the user's prompt. The output is a text-based trailer with characters, scene descriptions, and emotions that the user can utilize. This project aims to provide a new tool for filmmakers and creatives in the industry.", - tpms: [ - { name: "Amandeep Kaur" }, - { name: "Zahra Sarayloo" } - ], - collaboration: "Product Ventures", - }, - { - title: "Reinforcement Learning for Doom (1993)", - description: - "The project aims to build an RL agent that plays Doom (1993), improving upon the work from Playing FPS Games with Deep Reinforcement Learning (Lample & Chaplot 2017). They are currently implementing a DTQN model to train an agent in Vizdoom.", - tpms: [ - { name: "Karman Singh" }, - { name: "Krish Sethi" } - ], - }, - { - title: "Transfer learning to test decision-making generalisation in LLM agents", - description: - "We're aiming to create LLM agents that can make political decisions, like in parliamentary legislation. Our approach will attempt to train agents to make good decisions in gamified environments like Monopoly. We will then see how this training 'generalises' (transfers) to a real-life context using the case study of politics. The 'product' outcome is getting a good political decision-making agent. The 'research' outcome is testing the generalisation of the agent's decision making capabilities.", - tpms: [ - { name: "Mehar Shienh" }, - { name: "Madhav Malhotra" } - ], - }, - { - title: "WindDM: Conditional Diffusion Models for Super-resolution of Wind Data", - description: - "Access to high-quality, microscale data on local wind patterns is essential for determining optimal placements of wind farms. While this has traditionally been achieved through the use of Large Eddy Simulations (LES) to super-resolve mesoscale data, these are slow and costly to produce. We propose using recent advances in diffusion models to produce scalable and accurate microscale data at a fraction of the cost of LES models. We also leverage conditional information from the domain of interest to further improve our generations.", - tpms: [ - { name: "Daniel Bartman" }, - { name: "Jacob Schnell" } - ], - collaboration: "Veer Renewables", - }, - { - title: "PlayFitt Recommender System", - description: - "Our objective is to help people be more active! We developed a recommender system for the PlayFitt fitness app using a contextual bandit algorithm. This takes into account information about the users as context, and makes decisions about rep counts and reward values to suggest. Notably, this approach enables online learning.", - tpms: [ - { name: "Ben Bates" }, - { name: "Richard Wills" } - ], - collaboration: "Intellisports", - }, - { - title: "Solar Photovoltaic Output Prediction", - description: - "We set out to develop deep learning models that predict solar photovoltaic output. These forecasts on solar energy production can drive better energy decisions, massively reducing carbon emmissions produced by power grids.", - tpms: [ - { name: "Areel Khan" }, - { name: "Carter Demars" } - ], - collaboration: "Open Climate Fix", - }, - { - title: "Prostate Cancer Prediction With Correlated Diffusion Imaging", - description: - "The application of machine learning to medical images has led to impressive advancement in cancer diagnostics. Our objective is to develop a baseline of deep learning models to detect the presence of prostate cancer within a novel Correlated Diffusion Imaging (CDI) dataset.", - tpms: [ - { name: "Hargun Mujral" }, - { name: "Jarett Dewbury" } - ], - collaboration: "Dr. Alexander Wong & Hayden Gunraj", - }, - { - title: "Deep Learning Framework Comparison", - description: - "The goal of our project is to benchmark performance of several deep learning frameworks, including TensorFlow, PyTorch, Jax, MxNet, Flux.jl, and KNet.jl. We build models from scratch in each framework and test them on common datasets.", - tpms: [ - { name: "Anusha Raisinghani" }, - { name: "Trevor Yu" } - ], - }, - { - title: "Reinforcement Learning Chess Engine", - description: - "We are developing an artificial intelligence chess engine based on the work of Deep Mind on their chess engine━Alpha-Zero. We are also iteratively testing and modifying the model to improve performance on hardware with much more limited processing power than what was available to Deep Mind when creating Alpha-Zero.", - tpms: [ - { name: "Amya Singhal" }, - { name: "Thomas Fortin" } - ], - }, - { - title: "Stable Diffused Adversarial Attacks", - description: - "We are exploring pre-trained computer vision models (i.e. MobileNet_v1) with the goal to exploit vulnerabilities through various adversarial attacks. Our work aims to develop an architecture that automates adversarial attack image generation in model misclassification.", - tpms: [ - { name: "Andy Wu" }, - { name: "Dhrumil Patel" }, - { name: "Rayaq Siddiqui" } - ], - }, -]; diff --git a/src/data/projects.json b/src/data/projects.json new file mode 100644 index 0000000..587ecc8 --- /dev/null +++ b/src/data/projects.json @@ -0,0 +1,914 @@ +[ + { + "id": "chefost", + "started": "May 2026", + "term": "Summer 2026", + "year": 2026, + "title": "ChefOST", + "summary": "A method of video object-state tracking with continuous semantic representation, reduced to cooking videos. Given a user-specified ingredient, chefOST will propagate semantic object state forward through time, creating the ability to identify the state of (hopefully) any food in any video at any frame.", + "leads": [ + { + "name": "Harrison Fulford (MLE Shopify)", + "linkedin": "https://www.linkedin.com/in/harrison-fulford-147a1b2a9/" + }, + { + "name": "Rafael Fonseca (MLE Shopify)", + "linkedin": "https://www.linkedin.com/in/raf-fonseca/" + } + ], + "members": [ + "Justin Wang", + "Michelle Jeon", + "Ryan Li", + "Pritika Lahiri", + "Shray Kumar" + ], + "technologies": [ + "Computer Vision" + ], + "themes": [ + "Automation" + ], + "result": "In progress" + }, + { + "id": "weatherloo", + "started": "May 2026", + "term": "Summer 2026", + "year": 2026, + "title": "Weatherloo", + "summary": "We're building an automated ML pipeline to deliver accurate localized weather forecasts. Our custom ML-based weather models will be deployed and distributed to the community through our own weather app.", + "leads": [ + { + "name": "Cindy Li (SWE, Shopify)", + "linkedin": "https://linkedin.com/in/cindehaa/" + }, + { + "name": "Ayaan Rezwan", + "linkedin": "https://www.linkedin.com/in/ayaanrezwan/" + } + ], + "members": [], + "technologies": [ + "Deep Learning" + ], + "themes": [ + "Sustainability", + "Automation" + ], + "result": "In progress" + }, + { + "id": "worldfold", + "started": "May 2026", + "term": "Summer 2026", + "year": 2026, + "title": "WorldFold", + "summary": "WorldFold is a dual-arm robot which utilizes world modelling and reinforcement learning to fold cloth garments with unpredictable & chaotic deformations.", + "leads": [ + { + "name": "Adam Kamel" + }, + { + "name": "Vijay Goyal" + } + ], + "members": [ + "Zechariah Wang", + "Joshua Barre", + "Anushka Punukollu", + "Jacob Lu", + "Roy Suliaman", + "Mohammed Naqi", + "Ethan Fung", + "Ruby Zhao" + ], + "technologies": [ + "RL", + "Robotics" + ], + "themes": [ + "Hardware", + "Automation" + ], + "result": "In progress" + }, + { + "id": "epa-consultant-chatbot", + "started": "Jan 2026", + "term": "Winter 2026", + "year": 2026, + "title": "EPA Consultant Chatbot", + "summary": "An AI-powered, multi-agent regulatory consultant platform that structures global pesticide guidelines to automate compliance planning, gap analysis, and traceable Q&A.", + "leads": [ + { + "name": "Sachit Juneja", + "linkedin": "https://www.linkedin.com/in/sachit-singh-juneja/" + }, + { + "name": "William Cagas", + "linkedin": "https://www.linkedin.com/in/william-cagas/" + } + ], + "members": [ + "Fiona Cai", + "Richard Zhu", + "Lawrence Zou", + "Afreed Hassan", + "Shreya Sharma", + "Brandon Kong", + "Jinay Desai", + "Ricky Tang" + ], + "partnership": "Bindwell (YC W25)", + "technologies": [ + "Chatbots", + "GraphRAG" + ], + "themes": [ + "Sustainability" + ], + "result": "In progress" + }, + { + "id": "insightpulse", + "started": "Jan 2026", + "term": "Winter 2026", + "year": 2026, + "title": "InsightPulse", + "summary": "InsightPulse is a machine learning-driven market intelligence platform that analyzes macroeconomic indicators, asset performance, and market signals to classify the current market environment as risk-on, neutral, or risk-off. The system uses calculated features such as returns, volatility, moving averages, correlations, yields, inflation, oil prices, DXY, and VIX to generate market state predictions and plain-English explanations. It also includes a Scenario Playground, where users can adjust macro inputs like Fed Funds, CPI, GDP, unemployment, PMI, oil, DXY, and VIX, and the model estimates their potential impact on major assets and sectors.", + "leads": [ + { + "name": "Sharanya Basu (SWE Fellow.ai)", + "linkedin": "https://www.linkedin.com/in/sharanya-basu/" + } + ], + "members": [ + "Khalil Ahmad Qamar (MLOps Palitronica)", + "Lia Moradpour", + "Sai Sujit Kodakalla (SWE Shopify)", + "Krishna Jawale (SWE AltaML)", + "Nalin Verma", + "Deeptendu Shekhar Ray", + "Yousuf Rashid", + "Aradhana Vasudev" + ], + "technologies": [ + "Time series Analysis", + "Deep Learning" + ], + "themes": [ + "Finance" + ], + "result": "In progress", + "mediaUrl": "https://youtu.be/R5IKibZUFkI" + }, + { + "id": "flockrl", + "started": "Sep 2025", + "term": "Fall 2025", + "year": 2025, + "title": "FlockRL", + "summary": "Created an RL algorithm for obstacle detection and a custom drone flight simulator for testing, complete with physics, collision handling, and perception. Designed with the goal of investigating sim-to-real transfer onto resource-constrained hardware in relatively static environments like factory floors. Single-drone navigation was shipped with potential to extend into multi-drone coordination.", + "leads": [ + { + "name": "Katie Zhong", + "linkedin": "https://www.linkedin.com/in/katie-zhong/" + }, + { + "name": "Joshua Zhang (TikTok, Shopify MLE)", + "linkedin": "https://www.linkedin.com/in/joshualeezhang/" + } + ], + "members": [ + "Cindy Li (Shopify, Skyvern YC S23 MLE)", + "Wenzhao Pan (Shopify)", + "Claire Guo (Shopify)", + "David Zhong", + "Daniel Wei", + "Aadesh Kumar", + "Daniel Long", + "Lucas Jin (YC Startup School S26)", + "Lovera Lokeswara (Shopify)", + "Advitiya Sharma", + "Raiya Minhas (RBC SWE, RBC AI Eng)" + ], + "technologies": [ + "RL" + ], + "themes": [ + "Hardware" + ], + "result": "Github, demo", + "resultUrl": "https://github.com/FlockRL/simulator" + }, + { + "id": "politicalllm", + "started": "Sep 2025", + "term": "Fall 2025", + "year": 2025, + "title": "PoliticalLLM", + "summary": "Created LLM agents that can make political decisions, like in parliamentary legislation. Tested transferability of decision making skills in different environments (financial games like Monopoly, social games like Werewolf, political simulators, and also bill votes). Tested different agent setups for decision making (lawyers arguing in a courtroom, majority votes, etc.)", + "leads": [ + { + "name": "Mehar Shienh (ML for ads, Apple)", + "linkedin": "https://www.linkedin.com/in/mehar-shienh/" + }, + { + "name": "Madhav Malhotra (ML Infra, Tesla)", + "linkedin": "https://www.linkedin.com/in/madhav-malhotra/" + } + ], + "members": [ + "Jordan Leis (Cansbridge Fellow)", + "Devon Kisob", + "Evan Dennison", + "Yalda Nikookar", + "Jennifer Yu" + ], + "technologies": [ + "Chatbots" + ], + "themes": [ + "Legal", + "Automation" + ], + "result": "Github. Conference paper (won award at Ethical Tech for a Global Future Symposium)", + "resultUrl": "https://github.com/Madhav-Malhotra/political-chatbot" + }, + { + "id": "satellite-ml", + "started": "Sep 2024", + "term": "Fall 2024", + "year": 2024, + "title": "Satellite ML", + "summary": "We deployed AI on microcontrollers intended for satellites to identify and segment methane plumes using hyperspectral machine learning models. This works towards climate change mitigation by providing precise and real-time data on methane emissions.", + "leads": [ + { + "name": "Prahar Ijner (NeurIPS author)", + "linkedin": "https://www.linkedin.com/in/prahar-ijner/" + }, + { + "name": "Sarah Ali (ML Shopify)", + "linkedin": "https://www.linkedin.com/in/sarah-ali-cs/" + } + ], + "members": [ + "Chloe Zheng", + "Madeline Kim", + "Harsh Patel", + "Calista Besseling", + "Yasmeen Elkheir", + "Michelle Yao" + ], + "technologies": [ + "Computer Vision" + ], + "themes": [ + "Sustainability" + ], + "result": "Github, Environmental Sustainability award at CUCAI 2025", + "resultUrl": "https://github.com/WAT-ai/SatML" + }, + { + "id": "zoningllm", + "started": "Jan 2024", + "term": "Winter 2024", + "year": 2024, + "title": "ZoningLLM", + "summary": "We are building a webapp that uses GenAI Methods ( LLM+ RAG) to analyze zoning documents in Ontarian cities to gain insights about zoning regulation that help solve the Ontarian housing crisis.", + "leads": [ + { + "name": "Simha Kalimipalli (Masters in MLE at Sunnybrook Hospital)", + "linkedin": "https://www.linkedin.com/in/simha-kalimipalli/" + }, + { + "name": "Saurodeep Majumdar" + } + ], + "members": [ + "Liam Dachner", + "Jonathan Feng", + "Kevin Tan", + "Rahul Kumar" + ], + "partnership": "Smart Waterloo Innovation Lab, Hamming.ai (YC S24)", + "technologies": [ + "Chatbots" + ], + "themes": [ + "Sustainability", + "Automation" + ], + "result": "Created zoning bylaw analysis chatbot for use by partner lab" + }, + { + "id": "iot-cybersecurity", + "started": "Sep 2023", + "term": "Fall 2023", + "year": 2023, + "title": "IoT Cybersecurity", + "summary": "Created cyberdefence algorithms (network intrusion detection) for IoT devices. Compared genetic algorithms, random forests, SVMs, and artificial immune systems in an ablation study.", + "leads": [ + { + "name": "Madhav Malhotra (ML Infra, Tesla)", + "linkedin": "https://www.linkedin.com/in/madhav-malhotra/" + } + ], + "members": [ + "Ethan Lem (SWE Tesla)", + "Tian Yao (MLE Shopify, SWE Tesla)", + "Tim Kang (Shopify)", + "Zachary Wu", + "Akira Yoshiyama (AI research @ EthZurich, TheResidency)", + "Yen zein kok", + "Kenzy Soror" + ], + "technologies": [ + "General" + ], + "themes": [ + "Cybersecurity" + ], + "result": "Paper, Github, Substack blog posts", + "resultUrl": "https://docs.google.com/document/d/1PnSahadYfq6rKsXXNEp-1pKmIHV4AuvIE1m2IXexH6A/edit?tab=t.0" + }, + { + "id": "legacy-affinnity-predicting-drug-target-binding-with-graph-neural-networks", + "started": "", + "term": "Current project", + "year": 0, + "title": "AffiNNity: Predicting Drug–Target Binding with Graph Neural Networks", + "summary": "Drug discovery is slow, expensive, and often hit-or-miss. AffiNNity is a machine learning model designed to change that — using Graph Neural Networks (GNNs) to predict how strongly a drug will bind to a target protein. By combining molecular graphs with protein sequence data in a dual-stream setup, the system captures both the shape and behavior of drug–protein interactions. Built on top of large datasets like PDBBind and modern architectures like Graph Isomorphism Networks, AffiNNity learns the patterns that make a drug effective — without needing to run thousands of costly lab tests. The goal is to speed up early-phase drug screening, reduce experimental overhead, and help scientists identify the most promising candidates faster. As pharmaceutical pipelines increasingly rely on computational tools, AffiNNity offers a scalable, accurate way to bring life-saving treatments to patients more efficiently.", + "leads": [ + { + "name": "Jahkim Brown-Roopnarine", + "linkedin": "https://www.linkedin.com/in/jahkim-brown-roopnarine/" + }, + { + "name": "James Yu", + "linkedin": "https://www.linkedin.com/in/james-yu2005/" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "In progress" + }, + { + "id": "legacy-ai-sentiment-pulse-in-stock-market", + "started": "", + "term": "Current project", + "year": 0, + "title": "AI Sentiment Pulse in Stock Market", + "summary": "AI Sentiment Pulse in Stock Market is an application-focused project that leverages social media sentiment to anticipate short-term market trends. Inspired by the 2021 GameStop surge—where Reddit discussions triggered dramatic price shifts—we built a system to monitor real-time updates from subreddits like r/WallStreetBets. Instead of focusing on individual stocks, we analyze trending topics and collective user sentiment to detect sudden shifts in market attention. To improve accuracy, our system includes sarcasm detection tailored to the informal and ironic language often used in financial communities. These insights are combined with market data and processed using classical and deep learning models to forecast potential price movements. Our goal is to equip investors with timely, sentiment-driven signals that highlight emerging retail momentum.", + "leads": [ + { + "name": "Jiayou (Sam) Zhong", + "linkedin": "https://www.linkedin.com/in/jiayouz/" + }, + { + "name": "Shenyan (Bob) Zheng", + "linkedin": "https://www.linkedin.com/in/shenyan-zheng-0ab064274/" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "In progress" + }, + { + "id": "legacy-audio-temporal-segmentation-sentiment-analysis", + "started": "", + "term": "Past project", + "year": 0, + "title": "Audio Temporal Segmentation & Sentiment Analysis", + "summary": "Our project aims to analyze audio signals to detect phrases and understand emotions. We break down audio into smaller parts (called phrases) and study their patterns over time. This is done using a supervised dataset of audio with labeled phrases and different emotional tags corresponding to each phrase. By looking at the frequency and timing of sounds, we can predict and identify key phrases. We also apply language processing methods to analyze the emotional tone of the audio. This helps improve technologies like phrase detection and language analysis in various multimedia applications.", + "leads": [ + { + "name": "Krish Patel" + }, + { + "name": "Sahal Sajeer Kalandan" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-bci-signal-decoding-for-motor-control", + "started": "", + "term": "Past project", + "year": 0, + "title": "BCI Signal Decoding for Motor Control", + "summary": "We aim to develop a Spiking Neural Network that can translate neural activity from the brain's motor areas into muscle control commands. Using implanted electrode data from Macaques, our SNNs will provide insights for advanced prosthetic control and efficient brain-computer interface implementations.", + "leads": [ + { + "name": "Jakeb Chouinard" + }, + { + "name": "Raihan A. Vaheed" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-causal-modeling-and-time-series-representation-learning-for-diabetes-management", + "started": "", + "term": "Past project", + "year": 0, + "title": "Causal Modeling and Time Series Representation Learning for Diabetes Management", + "summary": "Gluroo aims to simplify diabetes management by streamlining the tracking of fitness, nutrition, and insulin use for people with diabetes (PWD). This project focuses on improving short-term prandial (meal-time) and postprandial blood glucose outcomes for people with type 1 diabetes, a complex disease that affects nearly 10 million people worldwide. We aim to leverage semi-supervised learning to identify unlabelled meals in time-series blood glucose data, develop meal-scoring functions, and explore causal machine-learning techniques. Our goal is to provide actionable insights to PWD and their care practitioners, enhancing health outcomes and quality of life.", + "leads": [ + { + "name": "Christopher Risi" + }, + { + "name": "Walker Payne" + }, + { + "name": "Dvir Zagury-Grynbaum" + } + ], + "members": [], + "partnership": "Gluroo", + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-clipabit-semantic-video-search-engine", + "started": "", + "term": "Current project", + "year": 0, + "title": "ClipABit: Semantic Video Search Engine", + "summary": "Video editors waste countless hours manually scrubbing through hundreds of footage files to find specific moments for their projects. ClipABit transforms this tedious process into seconds by enabling natural language search across entire video libraries. Simply describe what you're looking for: a sunset scene, a person laughing, or someone walking through a doorway, and instantly locate those exact moments. By making video content as searchable as text, ClipABit empowers creators to focus on storytelling instead of file management, dramatically accelerating the creative process for filmmakers, content creators, and media professionals.", + "leads": [ + { + "name": "Eshaan Mehta", + "linkedin": "https://www.linkedin.com/in/eshaan-mehta-136a6924b/" + }, + { + "name": "Safiya Makada", + "linkedin": "https://www.linkedin.com/in/safiya-makada/" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "In progress", + "resultUrl": "https://github.com/ClipABit" + }, + { + "id": "legacy-copyright-detection-in-large-language-models-an-ethical-approach-to-generative-ai-development", + "started": "", + "term": "Past project", + "year": 0, + "title": "Copyright Detection in Large Language Models - An ethical approach to Generative AI Development", + "summary": "Use of copyrighted content in training generative AI models has increased significantly as the field has emerged, however, it is challenging to know whether a model is using copyrighted material in its training data. There are two project goals The first is to develop tools to detect whether a piece of given content was used in training data of an LLM. The second is to build an open-source RAG-based logging system to keep track of pieces of flagged media that have been found in the training data of publicly available LLMs.", + "leads": [ + { + "name": "Senan Gaffori" + }, + { + "name": "Khushee Kapoor" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-deep-learning-framework-comparison", + "started": "", + "term": "Past project", + "year": 0, + "title": "Deep Learning Framework Comparison", + "summary": "The goal of our project is to benchmark performance of several deep learning frameworks, including TensorFlow, PyTorch, Jax, MxNet, Flux.jl, and KNet.jl. We build models from scratch in each framework and test them on common datasets.", + "leads": [ + { + "name": "Anusha Raisinghani" + }, + { + "name": "Trevor Yu" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-deep-learning-race-car", + "started": "", + "term": "Current project", + "year": 0, + "title": "Deep Learning Race Car", + "summary": "Self-driving technology could prevent millions of traffic deaths caused by human error each year. The Deep Learning Race Car project advances autonomous vehicle research by building a miniature race car that learns to navigate tracks independently. Racing serves as an ideal testing ground for autonomous systems because it demands split-second decisions and rapid adaptation to new environments. These challenges directly translate to real-world self-driving scenarios. By demonstrating how AI can master high-speed navigation in constrained spaces, this project contributes to making autonomous vehicles safer and more reliable for everyone.", + "leads": [ + { + "name": "Nina Zhang", + "linkedin": "https://www.linkedin.com/in/nina-zhang-a85935310/" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "In progress" + }, + { + "id": "legacy-deep-reinforcement-learning-for-stock-portfolio-optimization", + "started": "", + "term": "Past project", + "year": 0, + "title": "Deep Reinforcement Learning for Stock Portfolio Optimization", + "summary": "We are developing a reinforcement learning policy-agent model that optimizes a stock portfolio for long-term capital gains. This model will trade on stocks, commodities and indexes, and have access to real-time data on individual assets, as well as various indicators. In simpler words, we're making a model to make your Wealthsimple balance go up fast! 💰", + "leads": [ + { + "name": "Balambika Baskaran" + }, + { + "name": "Ali Elhor" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-delaynomore-ttc-bus-delay-forecaster", + "started": "", + "term": "Past project", + "year": 0, + "title": "DelayNoMore: TTC Bus Delay Forecaster", + "summary": "The goal of this project is to develop a model that accurately predicts whether a bus route will be delayed by leveraging previous years' TTC delay data which includes time of delay, location, route, vehicle number, and a few other fields. We also plan on expanding these fields by including seasonality, weather/road conditions, and other applicable features.", + "leads": [ + { + "name": "Ted Ferris" + }, + { + "name": "Chow Sheng Liang" + }, + { + "name": "Franklin Ramirez" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-energy-efficient-ai-accelerators-for-transformer-models", + "started": "", + "term": "Past project", + "year": 0, + "title": "Energy-efficient AI Accelerators for Transformer Models", + "summary": "We'll be creating an AI accelerator (a computer chip optimised to run AI models) for transformers. Our specific goal is to optimise the energy consumption of this AI accelerator, given the rising energy demand from data centres housing computer LLM inference. Our final digital accelerator design will be manufactured using Tiny Tapeout, whereas intermediate testing will occur using industry simulation softwares and field-programmable gate arrays (FPGAs).", + "leads": [ + { + "name": "Madhav Malhotra" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-fortif-ai-ai-driven-companion-for-senior-independence", + "started": "", + "term": "Current project", + "year": 0, + "title": "FORTif.ai: AI-Driven Companion for Senior Independence", + "summary": "FORTif.ai is an AI-driven companion that empowers seniors to live independently by merging proactive safety monitoring with tailored daily support. Using a computer-vision–powered Hazard Detection model, it continuously scans the home for potential risks—like spills, cluttered pathways, and tripping hazards—and offers clear, actionable recommendations to address them. At the same time, an intuitive AI chatbot engages users in friendly, proactive conversations, providing timely medication and appointment reminders, personalized wellness check-ins, and empathetic responses to questions or concerns. With built-in voice-to-text capabilities and real-time safety insights, FORTif.ai delivers a seamless, user-centric experience designed to enhance home safety, streamline everyday routines, and foster lasting independence for seniors.", + "leads": [ + { + "name": "Lino Kee", + "linkedin": "https://www.linkedin.com/in/linokee0423/" + }, + { + "name": "Edson Takei", + "linkedin": "https://www.linkedin.com/in/edsontakei/" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "In progress" + }, + { + "id": "legacy-microgrid-rl-autonomous-optimization-of-renewable-powered-microgrids", + "started": "", + "term": "Current project", + "year": 0, + "title": "Microgrid RL: Autonomous Optimization of Renewable-Powered Microgrids", + "summary": "Millions of people in rural Sub-Saharan Africa lack reliable access to electricity, hindering economic development and quality of life. Renewable-powered microgrids offer a solution, but managing the complex balance between solar generation, battery storage, and fluctuating demand remains challenging and costly. This project develops AI systems that autonomously optimize microgrid operations, making clean energy more reliable and affordable for off-grid communities. By reducing operational complexity and maximizing the use of renewable resources, this work directly contributes to expanding energy access in regions that need it most, enabling education, healthcare, and economic opportunities.", + "leads": [ + { + "name": "Jordan Leis", + "linkedin": "https://www.linkedin.com/in/jordan-leis" + }, + { + "name": "Devon Kisob", + "linkedin": "https://www.linkedin.com/in/devonkisob/" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "In progress" + }, + { + "id": "legacy-nuanceedge", + "started": "", + "term": "Past project", + "year": 0, + "title": "NuanceEdge", + "summary": "Open science is critical for a society's development and prosperity through knowledge sharing, collaboration, public trust, and evidence-informed decision making. It is currently difficult for policymakers to access and understand latest scientific results, making it difficult to appropriately inform decision making with the latest knowledge. Our team will be looking to address this problem by developing a web-based platform, NuanceEdge, aimed at making science more accessible through extraction of key insights and better presentation using generative AI tools.", + "leads": [ + { + "name": "Rachel Heo" + }, + { + "name": "Ethan Lem" + }, + { + "name": "Shruti Srivatsan" + } + ], + "members": [], + "partnership": "W&W", + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-oliver-ai-powered-virtual-teaching-assistant", + "started": "", + "term": "Current project", + "year": 0, + "title": "Oliver: AI-Powered Virtual Teaching Assistant", + "summary": "In recent years, advancements in conversational AI have led to the development of intelligent tutoring systems to enhance learning experiences through interactive conversation. This project, Oliver, is an innovative virtual teaching assistant and course management system that leverages contextual memory and response strategies designed to promote active learning and critical thinking. Unlike traditional models that frequently offer direct answers, Oliver encourages exploration and comprehension. Through our research paper, we underscore Oliver's potential to serve as a powerful tool in education, supporting learners in developing deeper cognitive skills rather than relying on rote memorization.", + "leads": [ + { + "name": "Haoran Zhu", + "linkedin": "https://www.linkedin.com/in/haoran-zhu-5243b0186/" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "In progress", + "resultUrl": "https://github.com/XiandaDu/WatAIOliver" + }, + { + "id": "legacy-pianofi-ai-powered-piano-transcription", + "started": "", + "term": "Current project", + "year": 0, + "title": "Pianofi: AI-Powered Piano Transcription", + "summary": "Musicians spend hours painstakingly transcribing songs by ear or settle for inaccurate auto-generated sheet music. PianoFi democratizes music learning by instantly transforming any song into professional-quality piano sheet music. Whether you're a beginner wanting to learn your favorite pop song or an advanced pianist seeking accurate transcriptions of complex pieces, PianoFi makes quality practice material accessible to everyone. By eliminating the barrier between hearing a song and being able to play it, this tool empowers musicians at all skill levels to learn, practice, and perform the music they love.", + "leads": [ + { + "name": "Jonathan Gong", + "linkedin": "https://www.linkedin.com/in/jonathan-gong-005491263/" + }, + { + "name": "Bruce Wang", + "linkedin": "https://www.linkedin.com/in/brucewang15/" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "In progress", + "resultUrl": "https://github.com/Pianofi" + }, + { + "id": "legacy-pitch-ai", + "started": "", + "term": "Past project", + "year": 0, + "title": "Pitch AI", + "summary": "We aim to develop a generative AI tool to create movie trailers for the media industry. We'll be using machine learning and deep learning to generate scenarios. The input parameters for the trailer generation include character ideas, plot points, and the user's prompt. The output is a text-based trailer with characters, scene descriptions, and emotions that the user can utilize. This project aims to provide a new tool for filmmakers and creatives in the industry.", + "leads": [ + { + "name": "Amandeep Kaur" + }, + { + "name": "Zahra Sarayloo" + } + ], + "members": [], + "partnership": "Product Ventures", + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-playfitt-recommender-system", + "started": "", + "term": "Past project", + "year": 0, + "title": "PlayFitt Recommender System", + "summary": "Our objective is to help people be more active! We developed a recommender system for the PlayFitt fitness app using a contextual bandit algorithm. This takes into account information about the users as context, and makes decisions about rep counts and reward values to suggest. Notably, this approach enables online learning.", + "leads": [ + { + "name": "Ben Bates" + }, + { + "name": "Richard Wills" + } + ], + "members": [], + "partnership": "Intellisports", + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-prostate-cancer-prediction-with-correlated-diffusion-imaging", + "started": "", + "term": "Past project", + "year": 0, + "title": "Prostate Cancer Prediction With Correlated Diffusion Imaging", + "summary": "The application of machine learning to medical images has led to impressive advancement in cancer diagnostics. Our objective is to develop a baseline of deep learning models to detect the presence of prostate cancer within a novel Correlated Diffusion Imaging (CDI) dataset.", + "leads": [ + { + "name": "Hargun Mujral" + }, + { + "name": "Jarett Dewbury" + } + ], + "members": [], + "partnership": "Dr. Alexander Wong & Hayden Gunraj", + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-radiel-health-personalized-medicine-through-computational-fluid-dynamics", + "started": "", + "term": "Current project", + "year": 0, + "title": "Radiel Health: Personalized Medicine Through Computational Fluid Dynamics", + "summary": "Surgical diagnosis has always been very observational, decisions were based off of what could be seen. There have been many successful attempts to add more quantitative metrics in medicine, but still diagnoses tend to be 'one-size-fits-all'. The next big leap in the field comes in the form of personalized medicine, where each patient has their own customized treatment that's shaped through empirical research. We are developing a platform where surgeons can upload ultrasounds of an artery, which is then turned into a 3D mesh and is run through our ML model. This model, trained on geometric and physics-based data from ultrasounds, allows us to quickly and accurately predict critical flow parameters for surgeries like coronary interventions. The goal is to minimize the cost to make these parameters available to surgeons from any clinic, making diagnoses more informed.", + "leads": [ + { + "name": "Rishabh Sharma", + "linkedin": "https://www.linkedin.com/in/rishabh2003sharma/" + }, + { + "name": "Ahash Ganeshamoorthy", + "linkedin": "https://www.linkedin.com/in/ahash-ganeshamoorthy/" + }, + { + "name": "Jatin Mehta", + "linkedin": "https://www.linkedin.com/in/jatin-r-mehta/" + } + ], + "members": [], + "partnership": "UW Fluid Flow Physics Group", + "technologies": [], + "themes": [], + "result": "In progress", + "resultUrl": "https://radielhealth.com/" + }, + { + "id": "legacy-reinforcement-learning-chess-engine", + "started": "", + "term": "Past project", + "year": 0, + "title": "Reinforcement Learning Chess Engine", + "summary": "We are developing an artificial intelligence chess engine based on the work of Deep Mind on their chess engine━Alpha-Zero. We are also iteratively testing and modifying the model to improve performance on hardware with much more limited processing power than what was available to Deep Mind when creating Alpha-Zero.", + "leads": [ + { + "name": "Amya Singhal" + }, + { + "name": "Thomas Fortin" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-reinforcement-learning-for-doom-1993", + "started": "", + "term": "Past project", + "year": 0, + "title": "Reinforcement Learning for Doom (1993)", + "summary": "The project aims to build an RL agent that plays Doom (1993), improving upon the work from Playing FPS Games with Deep Reinforcement Learning (Lample & Chaplot 2017). They are currently implementing a DTQN model to train an agent in Vizdoom.", + "leads": [ + { + "name": "Karman Singh" + }, + { + "name": "Krish Sethi" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-see-dr-mobile-diabetic-retinopathy-screening", + "started": "", + "term": "Current project", + "year": 0, + "title": "See-DR: Mobile Diabetic Retinopathy Screening", + "summary": "Diabetic retinopathy is a leading cause of preventable blindness, yet many at-risk patients lack access to regular eye screenings due to the cost and scarcity of specialized equipment. See-DR addresses this healthcare gap by transforming any smartphone into a portable screening device that can detect early signs of vision-threatening eye disease. By making screenings accessible in community clinics, pharmacies, and underserved areas, this tool has the potential to catch diabetic retinopathy before it causes irreversible damage, saving sight and improving quality of life for millions of people with diabetes worldwide.", + "leads": [ + { + "name": "Jessica Yuan", + "linkedin": "https://www.linkedin.com/in/jessica-yuan1/" + }, + { + "name": "Hari Chandrasekhar", + "linkedin": "https://www.linkedin.com/in/hari-chandrasekhar/" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "In progress", + "resultUrl": "https://github.com/jessicayuan1/see-dr" + }, + { + "id": "legacy-solar-photovoltaic-output-prediction", + "started": "", + "term": "Past project", + "year": 0, + "title": "Solar Photovoltaic Output Prediction", + "summary": "We set out to develop deep learning models that predict solar photovoltaic output. These forecasts on solar energy production can drive better energy decisions, massively reducing carbon emmissions produced by power grids.", + "leads": [ + { + "name": "Areel Khan" + }, + { + "name": "Carter Demars" + } + ], + "members": [], + "partnership": "Open Climate Fix", + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-stable-diffused-adversarial-attacks", + "started": "", + "term": "Past project", + "year": 0, + "title": "Stable Diffused Adversarial Attacks", + "summary": "We are exploring pre-trained computer vision models (i.e. MobileNet_v1) with the goal to exploit vulnerabilities through various adversarial attacks. Our work aims to develop an architecture that automates adversarial attack image generation in model misclassification.", + "leads": [ + { + "name": "Andy Wu" + }, + { + "name": "Dhrumil Patel" + }, + { + "name": "Rayaq Siddiqui" + } + ], + "members": [], + "technologies": [], + "themes": [], + "result": "Completed" + }, + { + "id": "legacy-winddm-conditional-diffusion-models-for-super-resolution-of-wind-data", + "started": "", + "term": "Past project", + "year": 0, + "title": "WindDM: Conditional Diffusion Models for Super-resolution of Wind Data", + "summary": "Access to high-quality, microscale data on local wind patterns is essential for determining optimal placements of wind farms. While this has traditionally been achieved through the use of Large Eddy Simulations (LES) to super-resolve mesoscale data, these are slow and costly to produce. We propose using recent advances in diffusion models to produce scalable and accurate microscale data at a fraction of the cost of LES models. We also leverage conditional information from the domain of interest to further improve our generations.", + "leads": [ + { + "name": "Daniel Bartman" + }, + { + "name": "Jacob Schnell" + } + ], + "members": [], + "partnership": "Veer Renewables", + "technologies": [], + "themes": [], + "result": "Completed" + } +] diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx index 5685758..f911d9a 100644 --- a/src/pages/Projects.tsx +++ b/src/pages/Projects.tsx @@ -1,10 +1,8 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useMemo, useState } from "react"; import { - Alert, Box, Button, Chip, - CircularProgress, Container, Grid, IconButton, @@ -15,48 +13,16 @@ import { Typography, useTheme, } from "@mui/material"; -import { ClearRounded, RefreshRounded, School, Science, SearchRounded, TrendingUp } from "@mui/icons-material"; +import { ClearRounded, School, Science, SearchRounded, TrendingUp } from "@mui/icons-material"; import ModernProjectCard from "../components/ModernProjectCard"; -import { loadProjectsFromSheet, SheetProject } from "../services/projectSheet"; - -const CACHE_KEY = "watai-projects-sheet-cache-v1"; +import { projects } from "../services/projectData"; const Projects: React.FC = () => { const theme = useTheme(); - const [projects, setProjects] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); - const [reloadToken, setReloadToken] = useState(0); const [search, setSearch] = useState(""); const [technology, setTechnology] = useState("All technologies"); const [themeFilter, setThemeFilter] = useState("All themes"); - const [resultFilter, setResultFilter] = useState<"All" | "In progress" | "Results">("All"); - - useEffect(() => { - let active = true; - setLoading(true); - setError(""); - - loadProjectsFromSheet() - .then((data) => { - if (!active) return; - setProjects(data); - localStorage.setItem(CACHE_KEY, JSON.stringify(data)); - }) - .catch((loadError: Error) => { - if (!active) return; - const cached = localStorage.getItem(CACHE_KEY); - if (cached) { - setProjects(JSON.parse(cached)); - setError("Showing the most recently saved project data because the live sheet is temporarily unavailable."); - } else { - setError(loadError.message); - } - }) - .finally(() => active && setLoading(false)); - - return () => { active = false; }; - }, [reloadToken]); + const [resultFilter, setResultFilter] = useState<"All" | "In progress" | "Completed">("All"); const technologies = useMemo(() => Array.from(new Set(projects.flatMap((project) => project.technologies))).sort(), [projects]); const themes = useMemo(() => Array.from(new Set(projects.flatMap((project) => project.themes))).sort(), [projects]); @@ -96,7 +62,7 @@ const Projects: React.FC = () => { { value: inProgressCount, label: "Active Projects", icon: }, { value: resultCount, label: "Past Projects", icon: }, ].map((stat) => ( - {stat.icon}{loading ? "–" : stat.value}{stat.label} + {stat.icon}{stat.value}{stat.label} ))} @@ -118,16 +84,12 @@ const Projects: React.FC = () => { - {(["All", "In progress", "Results"] as const).map((filter) => setResultFilter(filter)} sx={{ color: resultFilter === filter ? "#111" : theme.palette.text.secondary, backgroundColor: resultFilter === filter ? theme.palette.primary.main : "rgba(255,255,255,0.05)", fontWeight: 700 }} />)} + {(["All", "In progress", "Completed"] as const).map((filter) => setResultFilter(filter)} sx={{ color: resultFilter === filter ? "#111" : theme.palette.text.secondary, backgroundColor: resultFilter === filter ? theme.palette.primary.main : "rgba(255,255,255,0.05)", fontWeight: 700 }} />)} {(search || technology !== "All technologies" || themeFilter !== "All themes" || resultFilter !== "All") && } - {error && } onClick={() => setReloadToken((value) => value + 1)}>Retry} sx={{ mb: 4 }}>{error}} - - {loading ? ( - - ) : filteredProjects.length ? ( + {filteredProjects.length ? ( {filteredProjects.map((project) => )} diff --git a/src/services/projectData.ts b/src/services/projectData.ts new file mode 100644 index 0000000..44a5404 --- /dev/null +++ b/src/services/projectData.ts @@ -0,0 +1,4 @@ +import projectData from "../data/projects.json"; +import { Project } from "../types/project"; + +export const projects = projectData as Project[]; diff --git a/src/services/projectSheet.ts b/src/services/projectSheet.ts deleted file mode 100644 index 116472d..0000000 --- a/src/services/projectSheet.ts +++ /dev/null @@ -1,251 +0,0 @@ -export const PROJECT_SHEET_ID = "1y95UWwpNNwWkoivU2j3jt1q3JwBG-OCpxUjVfW-WV5w"; -export const PROJECT_SHEET_GID = "1142127646"; -const PROJECTS_API_URL = process.env.REACT_APP_PROJECTS_API_URL?.trim() - || "https://script.google.com/macros/s/AKfycbxrq50_YqA2gwj_r-CIECvsVsFZVDeYq1vBfajUJHoaCUEHufunx1qsx2ptC9F__fHv/exec"; - -export interface SheetProjectLead { - name: string; - linkedin?: string; -} - -export interface SheetProject { - id: string; - started: string; - term: string; - year: number; - title: string; - summary: string; - leads: SheetProjectLead[]; - members: string[]; - partnership?: string; - technologies: string[]; - themes: string[]; - result: string; - resultUrl?: string; - mediaUrl?: string; -} - -interface GvizCell { - v?: string | number | null; - f?: string; -} - -interface GvizResponse { - status: string; - errors?: Array<{ message?: string }>; - table?: { - cols: Array<{ label?: string }>; - rows: Array<{ c: Array }>; - }; -} - -interface AppsScriptResponse { - status: string; - projects: Array<{ - started?: string; - title?: string; - summary?: string; - leads?: SheetProjectLead[]; - members?: string[]; - partnership?: string; - technologies?: string[]; - themes?: string[]; - result?: string; - resultUrl?: string; - mediaUrl?: string; - }>; -} - -let requestSequence = 0; - -const clean = (value: unknown) => - String(value ?? "") - .replace(/\u00a0/g, " ") - .replace(/\s+/g, " ") - .trim(); - -const splitList = (value: string) => { - const source = clean(value).replace(/\s+and\s+/gi, ", "); - const items: string[] = []; - let current = ""; - let parenthesesDepth = 0; - - for (const character of source) { - if (character === "(") parenthesesDepth += 1; - if (character === ")") parenthesesDepth = Math.max(0, parenthesesDepth - 1); - - if ((character === "," || character === "\n") && parenthesesDepth === 0) { - if (clean(current)) items.push(clean(current)); - current = ""; - } else { - current += character; - } - } - - if (clean(current)) items.push(clean(current)); - return items; -}; - -const normalizePartnership = (value: string) => { - const normalized = clean(value); - return /^(none|n\/a|-)?$/i.test(normalized) ? undefined : normalized; -}; - -const getTerm = (started: string) => { - const normalized = clean(started); - const year = Number(normalized.match(/\b(20\d{2})\b/)?.[1] || 0); - const month = normalized.toLowerCase(); - const season = month.startsWith("jan") - ? "Winter" - : month.startsWith("may") - ? "Spring" - : month.startsWith("sep") - ? "Fall" - : normalized.replace(/\s*20\d{2}.*/, ""); - - return { term: [season, year || ""].filter(Boolean).join(" "), year }; -}; - -const slugify = (value: string) => - value - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/(^-|-$)/g, ""); - -const findUrl = (value: string) => value.match(/https?:\/\/[^\s,]+/i)?.[0]; - -const mapResponse = (response: GvizResponse): SheetProject[] => { - if (response.status !== "ok" || !response.table) { - throw new Error(response.errors?.[0]?.message || "Google Sheets returned an invalid response."); - } - - const headers = response.table.cols.map((column) => clean(column.label).toLowerCase()); - const columnIndex = (...names: string[]) => - headers.findIndex((header) => names.some((name) => header === name || header.includes(name))); - - const indexes = { - started: columnIndex("started"), - title: columnIndex("project title"), - summary: columnIndex("project summary"), - leads: columnIndex("project leads"), - members: columnIndex("project members"), - partnership: columnIndex("partnership"), - technology: columnIndex("technology"), - theme: columnIndex("theme"), - result: columnIndex("links and results"), - media: columnIndex("demo media", "demo video", "demo image", "media"), - }; - - const cellValue = (cells: Array, index: number) => { - if (index < 0) return ""; - const cell = cells[index]; - return clean(cell?.f ?? cell?.v ?? ""); - }; - - return response.table.rows - .map((row) => { - const started = cellValue(row.c, indexes.started); - const title = cellValue(row.c, indexes.title); - const summary = cellValue(row.c, indexes.summary); - const result = cellValue(row.c, indexes.result); - const mediaUrl = findUrl(cellValue(row.c, indexes.media)); - const { term, year } = getTerm(started); - - return { - id: `${slugify(title)}-${slugify(term)}`, - started, - term, - year, - title, - summary, - leads: splitList(cellValue(row.c, indexes.leads)).map((name) => ({ name })), - members: splitList(cellValue(row.c, indexes.members)), - partnership: normalizePartnership(cellValue(row.c, indexes.partnership)), - technologies: splitList(cellValue(row.c, indexes.technology)), - themes: splitList(cellValue(row.c, indexes.theme)), - result, - resultUrl: findUrl(result), - mediaUrl, - }; - }) - .filter((project) => project.title && project.summary) - .sort((a, b) => b.year - a.year || b.started.localeCompare(a.started)); -}; - -const mapAppsScriptResponse = (response: AppsScriptResponse): SheetProject[] => { - if (response.status !== "ok" || !Array.isArray(response.projects)) { - throw new Error("The projects API returned an invalid response."); - } - - return response.projects - .map((project) => { - const started = clean(project.started); - const title = clean(project.title); - const summary = clean(project.summary); - const result = clean(project.result); - const { term, year } = getTerm(started); - return { - id: `${slugify(title)}-${slugify(term)}`, - started, - term, - year, - title, - summary, - leads: (project.leads || []).map((lead) => ({ name: clean(lead.name), linkedin: clean(lead.linkedin) || undefined })).filter((lead) => lead.name), - members: (project.members || []).map(clean).filter(Boolean), - partnership: normalizePartnership(clean(project.partnership)), - technologies: (project.technologies || []).map(clean).filter(Boolean), - themes: (project.themes || []).map(clean).filter(Boolean), - result, - resultUrl: clean(project.resultUrl) || findUrl(result), - mediaUrl: findUrl(clean(project.mediaUrl)), - }; - }) - .filter((project) => project.title && project.summary) - .sort((a, b) => b.year - a.year || b.started.localeCompare(a.started)); -}; - -export const loadProjectsFromSheet = () => - new Promise((resolve, reject) => { - const callbackName = `__wataiProjectsCallback_${Date.now()}_${requestSequence++}`; - const callbackHost = window as unknown as Record void) | undefined>; - const script = document.createElement("script"); - const timeout = window.setTimeout(() => { - cleanup(); - reject(new Error("The project sheet took too long to respond.")); - }, 12000); - - const cleanup = () => { - window.clearTimeout(timeout); - delete callbackHost[callbackName]; - script.remove(); - }; - - callbackHost[callbackName] = (response) => { - try { - resolve("projects" in response ? mapAppsScriptResponse(response) : mapResponse(response)); - } catch (error) { - reject(error); - } finally { - cleanup(); - } - }; - - script.onerror = () => { - cleanup(); - reject(new Error("Unable to load projects from Google Sheets.")); - }; - - if (PROJECTS_API_URL) { - const separator = PROJECTS_API_URL.includes("?") ? "&" : "?"; - script.src = `${PROJECTS_API_URL}${separator}callback=${encodeURIComponent(callbackName)}`; - } else { - const query = new URLSearchParams({ - gid: PROJECT_SHEET_GID, - tqx: `responseHandler:${callbackName}`, - headers: "1", - }); - script.src = `https://docs.google.com/spreadsheets/d/${PROJECT_SHEET_ID}/gviz/tq?${query}`; - } - document.head.appendChild(script); - }); diff --git a/src/types/project.ts b/src/types/project.ts new file mode 100644 index 0000000..2d88235 --- /dev/null +++ b/src/types/project.ts @@ -0,0 +1,21 @@ +export interface ProjectLead { + name: string; + linkedin?: string; +} + +export interface Project { + id: string; + started: string; + term: string; + year: number; + title: string; + summary: string; + leads: ProjectLead[]; + members: string[]; + partnership?: string; + technologies: string[]; + themes: string[]; + result: string; + resultUrl?: string; + mediaUrl?: string; +} From 8d94242e9c1e2e1cdda9c324dc42f7cc915aefaa Mon Sep 17 00:00:00 2001 From: Sharanya Basu Date: Tue, 15 Sep 2026 21:26:07 -0400 Subject: [PATCH 4/4] Added yellow border to filters --- src/pages/Projects.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/pages/Projects.tsx b/src/pages/Projects.tsx index f911d9a..ffcacd9 100644 --- a/src/pages/Projects.tsx +++ b/src/pages/Projects.tsx @@ -24,8 +24,8 @@ const Projects: React.FC = () => { const [themeFilter, setThemeFilter] = useState("All themes"); const [resultFilter, setResultFilter] = useState<"All" | "In progress" | "Completed">("All"); - const technologies = useMemo(() => Array.from(new Set(projects.flatMap((project) => project.technologies))).sort(), [projects]); - const themes = useMemo(() => Array.from(new Set(projects.flatMap((project) => project.themes))).sort(), [projects]); + const technologies = useMemo(() => Array.from(new Set(projects.flatMap((project) => project.technologies))).sort(), []); + const themes = useMemo(() => Array.from(new Set(projects.flatMap((project) => project.themes))).sort(), []); const filteredProjects = useMemo(() => { const query = search.trim().toLowerCase(); @@ -37,7 +37,7 @@ const Projects: React.FC = () => { && (themeFilter === "All themes" || project.themes.includes(themeFilter)) && matchesResult; }); - }, [projects, search, technology, themeFilter, resultFilter]); + }, [search, technology, themeFilter, resultFilter]); const clearFilters = () => { setSearch(""); @@ -48,6 +48,13 @@ const Projects: React.FC = () => { const resultCount = projects.filter((project) => project.result && !/^in progress$/i.test(project.result)).length; const inProgressCount = projects.filter((project) => /^in progress$/i.test(project.result)).length; + const selectFilterSx = { + "& .MuiOutlinedInput-root": { + "& fieldset": { borderColor: `${theme.palette.primary.main}80` }, + "&:hover fieldset": { borderColor: theme.palette.primary.main }, + "&.Mui-focused fieldset": { borderColor: theme.palette.primary.main }, + }, + }; return ( @@ -73,12 +80,12 @@ const Projects: React.FC = () => { setSearch(event.target.value)} placeholder="Search projects, leads, technology..." InputProps={{ startAdornment: , endAdornment: search ? setSearch("")}> : undefined }} sx={{ "& .MuiOutlinedInput-root": { backgroundColor: "rgba(255,255,255,0.025)", borderRadius: 2.5 } }} /> - setTechnology(event.target.value)} label="Technology"> + setTechnology(event.target.value)} label="Technology" sx={selectFilterSx}> All technologies{technologies.map((item) => {item})} - setThemeFilter(event.target.value)} label="Theme"> + setThemeFilter(event.target.value)} label="Theme" sx={selectFilterSx}> All themes{themes.map((item) => {item})}