# Project export: UrbanPilot

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: UrbanPilot — AI agents that analyze any current address for climate, accessibility, and housing risk, then show you 2040 and 2075 predictions of what could come next.
- Devpost: https://devpost.com/software/urbanpilot
- GitHub: https://github.com/KangJustin/urbanpilot
- Video: https://www.youtube.com/embed/amuAEO7ki_I?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — KangJustin (54 commits), Claude Sonnet 4.6 (44 commits), kdai05 (16 commits)

## Devpost submission (written by the team)

### Inspiration

Cities are changing faster than ever due to climate change, housing shortages, rising temperatures, flooding, and shifting transportation needs. Yet most residents, planners, and policymakers struggle to access planning data because it is scattered across dozens of government websites, reports, and GIS platforms. As students interested in urban planning, data science, design, and artificial intelligence, we wanted to create a tool that makes complex planning information understandable for everyone. We asked a simple question: What if anyone could enter an address and instantly understand both its current conditions and its future risks? UrbanPilot was built to bridge the gap between public data and public understanding.

### What it does

UrbanPilot analyzes any address and combines real public planning data with specialized AI agents to explain current conditions, identify risks, and visualize how a neighborhood could evolve by 2040 and 2075. Unlike traditional planning tools, UrbanPilot grounds its analysis in verified public datasets including Census, FEMA, GTFS transit, NLCD tree canopy, and Open-Meteo environmental data, while clearly distinguishing between measured data and AI-generated projections. The platform evaluates: Climate risk Flood risk Heat vulnerability Housing conditions Accessibility and transit access Environmental factors Community resilience indicators Each address gets one overall score, averaged only from the categories with valid data: $$\text{Overall Score} = \frac{1}{n}\sum_{i \,\in\, {\text{climate, accessibility, housing}}} \text{Score}_i$$ Users can explore both current conditions and projected future scenarios for 2040 and 2075. The platform combines verified public datasets with AI-generated planning recommendations while clearly distinguishing between measured data and projected estimates. UrbanPilot also generates AI-created future visualizations showing how neighborhoods may evolve under changing environmental conditions, helping users understand future impacts in a way that maps and spreadsheets cannot. Finally, users can chat with UrbanPilot AI to ask planning-related questions and receive evidence-based recommendations grounded in verified planning data.

### How we built it

We built UrbanPilot as a full-stack web application using: React for the frontend Node.js and Express for the backend Tailwind CSS and shadcn/ui for the frontend design system Leaflet + OpenStreetMap/CARTO for the interactive map (Google Maps is used server-side only, for Street View/satellite imagery and geocoding) Climate and flood risk datasets Specialized AI agents for housing, accessibility, climate, vision generation, and planning assistance Claude and Anthropic APIs for reasoning and recommendations Multi-agent workflows for data collection, analysis, and report generation Midjourney's MCP server for AI-generated future neighborhood visualizations Sentry for production error monitoring ACS Census 2024 5-Year estimates 511 SF Bay GTFS transit data FEMA National Flood Hazard Layer NLCD Tree Canopy Cover 2023 Open-Meteo weather and air quality data The Token Company's prompt-compression SDK, layered on top of a custom-built compact prompt encoding, to reduce per-request token cost The system combines five public datasets, geospatial analysis, and specialized AI agents into a single workflow that transforms a simple address into actionable planning insights. We also treated token efficiency as a first-class engineering problem, not an afterthought. Every agent prompt that sends verified third-party data (Census, FEMA, NLCD, GTFS) to Claude was rewritten from a pretty-printed, human-readable format into a hand-built compact encoding — measured against real Anthropic API calls, this cut input tokens by 27.8–35.4% per agent with zero loss of output quality (every response still parsed as valid JSON and cited the exact verified figures). We then layered The Token Company's compression SDK on top of that in production, and benchmarked both independently: their generic compressor performs best on prose-heavy, unstructured prompts (20.6% reduction on our AI assistant's context), while our domain-specific encoding outperforms it by roughly 7–9x on structured, schema-heavy prompts — because it can safely remove formatting overhead that a generic compressor has to leave alone. Project Background UrbanPilot began as a simple location-search prototype with Google Maps integration and address lookup functionality. During the hackathon, we built the core platform functionality, including the multi-agent planning architecture, public data integrations, future scenario generation, AI planning assistant, climate and accessibility analysis, visualization pipeline, and data-grounded recommendation system that define UrbanPilot today. What we built during the hackathon New functionality built during the event includes: Multi-agent planning architecture ACS Census data integration and grounding Housing Agent grounded with ACS Census data Climate Agent grounded with weather, air quality, flood, and canopy data Accessibility Agent grounded with GTFS transit data FEMA flood risk integration GTFS transit accessibility integration Tree canopy analysis Future scenario generation for 2040 and 2075 AI-powered planning assistant Data transparency and source disclosure framework Planning recommendation engine New dashboard and analysis workflow Unified urban planning scoring framework AI-generated future neighborhood visualizations using Midjourney Token-efficient prompt engineering and a production integration with The Token Company's compression SDK These additions introduced the core intelligence, planning analysis, forecasting, and decision-support capabilities that define UrbanPilot today. ##

### Challenges we ran into

One of our biggest challenges was integrating multiple planning and environmental datasets that often use different formats, scales, and geographic boundaries. Another challenge was ensuring our AI recommendations remained grounded in real planning data rather than generating unsupported conclusions. We also faced difficulties creating realistic future visualizations that accurately reflected environmental risks while remaining understandable to everyday users. A major challenge was preventing AI hallucinations. We wanted UrbanPilot to provide useful planning recommendations without presenting AI-generated estimates as verified facts. This led us to build a data disclosure framework that explicitly separates grounded public data from AI-generated analysis. Finally, coordinating frontend mapping systems, backend APIs, and AI agents within the hackathon timeframe required significant debugging and iteration. Claude doesn't always return perfectly-formed JSON, especially under truncation — we built a fallback parser per agent that can recover a partial, truncated response into valid structured data rather than failing the entire analysis over one malformed character. Integrating a third-party SDK into the same client every agent depends on also meant vetting it carefully before trusting it with real request data — checking package provenance, confirming what it actually transmits and to where, and working through an ESM/CommonJS interop issue before it was safe to wire into production. ##

### Accomplishments we're proud of

Transformed an early planning prototype into a data-grounded AI urban planning platform with multi-agent analysis, future scenario generation, and climate risk forecasting. Grounded AI analysis using real Census, transit, flood, tree canopy, weather, and air quality datasets. Generated future-focused neighborhood visualizations for 2040 and 2075. Created an interactive map-based experience with address-level analysis. Developed a system that makes complex planning information accessible to non-experts. Combined urban planning, climate science, GIS, and AI into a single platform. Integrated five independent public data sources into a unified planning workflow. Built a transparent AI system that distinguishes verified data from AI-generated recommendations. Created an address-level planning tool that combines housing, climate, accessibility, and future scenario generation in a single experience. Cut LLM input-token cost per agent call by up to 35% through a custom compact encoding, verified with real API calls rather than estimates, with zero loss of output accuracy. Each specialist agent fails independently — if one data source or Claude call goes down, that section shows as unavailable while the rest of the analysis completes normally. ##

### What we learned

Through UrbanPilot, we learned how difficult it is to translate technical planning and environmental data into tools that ordinary people can understand. We learned that trustworthy AI requires transparency. To address this, UrbanPilot distinguishes between verified data sources and AI-generated estimates through a built-in data disclosure framework. We also gained experience building multi-agent AI systems, integrating geospatial datasets, creating AI-generated visualizations, and designing interfaces that communicate uncertainty responsibly. We also learned that compression isn't one-size-fits-all: generic, automatic compression and domain-specific prompt engineering solve different problems, and the biggest token savings came from understanding why a prompt was expensive, not just bolting on a tool. Most importantly, we learned that AI can be a powerful tool for civic engagement when paired with trustworthy public data. We also learned that transparency is just as important as accuracy when building AI systems intended to support real-world decision making. ##

### What's next

Our vision is to turn UrbanPilot into a comprehensive civic intelligence platform. Future plans include: Additional climate adaptation scenarios Neighborhood comparison tools Property-level resilience scoring Transit and infrastructure forecasting Community planning collaboration features Citywide planning dashboards for governments and nonprofits Expanded support for cities across the United States We also plan to expand beyond the Bay Area by supporting additional regions and incorporating more local planning datasets so UrbanPilot can serve communities nationwide. Long-term, we hope UrbanPilot can help communities make more informed decisions about where they live, invest, and build for the future.

## README (from the GitHub repository)

# UrbanPilot

A multi-agent urban planning copilot. Search any real address, get an AI-driven analysis
across climate resilience, accessibility, and housing, and see Current/2040/2075 scenarios —
Current backed by a real Street View/satellite photo of the site, 2040/2075 generated by
Midjourney.

## Architecture

```text
src/                      React frontend (Create React App + Tailwind + Leaflet)
  App.js                  App shell — state/handlers, composes the components below
  components/
    TopHeader.js             Logo, compact location search, live conditions bar
    LocationSearch.js        Google Places Autocomplete (proxied through the backend)
    ConditionsBar.js         Live weather/AQI/heat/flood badges
    ControlStrip.js          Planning-goal / target-year picker + Analyze trigger
    AnalysisStatusBar.js     Compact status line while agents are running
    ReadyToAnalyzeCard.js    Pre-analysis onboarding state — no fabricated scores/data
    MainMapPanel.js          Leaflet map workspace: marker, zoom controls, scenario overlay image
    ProjectedScenarioChanges.js  Per-scenario projected-change stat strip, beside the map
    StreetViewPanel.js       Collapsible wrapper around PresentDayView
    PresentDayView.js        Google Maps JS API panel: Street View / satellite toggle
    VisualizeStreetscapeAction.js  "Visualize Proposed Streetscape" trigger (Midjourney)
    ReferenceImageInput.js   Upload-your-own-photo workflow for the Midjourney reference image
    CurrentConditionsPanel.js  Verified climate/accessibility/housing snapshot
    PlanningFindings.js      Tabbed container: Risks / Recommendations / Interventions
    RisksPanel.js, RecommendationsPanel.js, InterventionsPanel.js, InterventionCard.js
                             Right-hand analysis panel content (gated behind a completed analysis)
    ScoreBreakdownPanel.js   Per-category score breakdown
    DataMethodologySection.js  Collapsed-by-default section hosting the 4 full AgentCard.js cards
    AgentCard.js             One card per specialist agent (climate/accessibility/housing/urban design)
    AIAssistantPanel.js      Docked Ask-AI chat panel
    ui/                      shadcn/ui primitives (Card, Badge, Tabs, Tooltip, ScrollArea)
  constants/planning.js    Default location + shared planning constants
  lib/utils.js             shadcn's cn() className helper
  utils/                   formatters.js (null-safe display formatting), planningHelpers.js
                           (cost/weather icon + color maps)
  services/analysisApi.js  All fetch calls to the backend, in one place

server/                   Node/Express backend
  agents/                 One file per Claude agent (climate, accessibility, housing,
                          urbanDesign, vision, ask) + coordinator.js orchestrating them
  routes/                 analysis, ask, conditions, location, upload, visualize, health
  services/
    claudeService.js      Wraps the Anthropic SDK; the client is wrapped again with
                          the-token-company's withCompression (see Token compression below)
    promptCompression.js  Shared compact-encoding helpers used by the housing/climate/
                          accessibility agent prompts
    censusService.js      U.S. Census Geocoder → block group → ACS 5-Year housing metrics
    openMeteoService.js   Live weather + US AQI for Climate Agent grounding (no key needed)
    femaNfhlService.js    FEMA National Flood Hazard Layer flood-zone lookup (no key needed)
    nlcdTccService.js     NLCD Tree Canopy Cover lookup (no key needed)
    transit511Service.js  511 SF Bay Regional GTFS → verified transit proximity metrics
    *AgentParser.js       Per-agent (housing/climate/accessibility) JSON extraction +
                          fallback repair for truncated/malformed Claude responses
    conditionsService.js Live weather/AQI via Open-Meteo (no key needed) — powers the
                          frontend's top "Live Data" conditions bar specifically
    googleMapsService.js Places (New) Autocomplete/Details, Street View status, image proxies
    midjourneyMcpClient.js
                          OAuth + connection management for Midjourney's MCP server
    midjourneyService.js generateImage() — the actual Midjourney call
    renderingProvider.js FutureRenderingProvider abstraction over midjourneyService
  scripts/                One-off scripts that hit real external APIs: verify-housing-census.js,
                          verify-climate-{fema,nlcd,openmeteo}.js, verify-accessibility-transit.js,
                          verify-ask-grounding.js, verify-vision-baselines.js, plus the token
                          compression benchmark (compression-bench.js, compression_benchmark_ttc*.py)
```

**Data flow for an analysis:** `LocationSearch` resolves an address to `{placeId, formattedAddress, latitude, longitude, viewport}` (the single source of truth, `selectedLocation` in `App.js`) → `/api/analyze` runs the climate/accessibility/housing/urbanDesign/vision agents in parallel, each grounding itself in a real verified data source (Census ACS, Open-Meteo, FEMA NFHL, NLCD, 511 GTFS) before asking Claude about the site → results populate the AI agent cards, Score Breakdown, Top Risks, and Top Recommendations panels (all empty/idle until that analysis completes — there's no bundled demo data to fall back to). The Current scenario shows a real photo (Street View if covered, otherwise satellite) fetched through `/api/location/street-view-image` and `/api/location/satellite-image` — these proxy routes exist so the Google API key never reaches the browser. 2040/2075 generate via Midjourney, using that same real photo as a composition reference by default (or your own uploaded photo).

A diagram of the agent pipeline (parallel specialist agents → synthesis → vision → response) is in
[`docs/agent-workflow.png`](docs/agent-workflow.png).

## Setup

```bash
git clone https://github.com/KangJustin/urbanpilot.git
cd urbanpilot
npm install
cd server && npm install && cd ..
```

Copy the two `.env.example` files and fill in real values:

```bash
cp .env.example .env
cp server/.env.example server/.env
```

### Required environment variables

| Variable | Where | What it's for |
|---|---|---|
| `ANTHROPIC_API_KEY` | `server/.env` | Powers every Claude agent. Without it, each agent's Claude call fails and that section of the analysis shows as temporarily unavailable rather than a result (there's no mock-data fallback). |
| `GOOGLE_MAPS_SERVER_API_KEY` | `server/.env` | Server-only key for Places API (New), Geocoding API, Street View Static API, Maps Static API. **Never** put this in the frontend. |
| `CENSUS_API_KEY` | `server/.env` | U.S. Census Bureau key for the Housing Agent's verified ACS metrics. Without it, the Housing Agent still runs, just without verified Census grounding. |
| `TRANSIT_511_API_KEY` | `server/.env` | 511 SF Bay Open Data key for the Accessibility Agent's verified GTFS transit metrics. Same degrade-gracefully behavior without it. |
| `TTC_API_KEY` | `server/.env` | The Token Company key. `claudeService.js` wraps the Anthropic client with their `withCompression` on every agent call — see [Token compression](#token-compression-the-token-company) below. |
| `REACT_APP_GOOGLE_MAPS_API_KEY` | `.env` (root) | Client-side key for the Maps JavaScript API, used specifically by `PresentDayView.js`'s Street View/satellite panel (the main map workspace itself is Leaflet, not Google Maps JS). Restrict it by HTTP referrer in Google Cloud Console — it's visible in the browser by design. |

Optional:

| Variable | Where | What it's for |
|---|---|---|
| `MIDJOURNEY_OAUTH_PORT` | `server/.env` | Local callback port for the one-time Midjourney OAuth login (default `8090`). |
| `ALLOWED_ORIGINS` | `server/.env` | Comma-separated extra CORS origins, e.g. for sharing over LAN. |
| `REACT_APP_API_URL` | `.env` (root) | Override the backend URL the frontend calls (de

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 103 recognized source files, 354 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- Redis (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (111 of 111)

```
.env.example
.gitignore
App.js
design-system/MASTER.md
docs/agent-workflow.mmd
index.css
index.js
package.json
postcss.config.js
public/index.html
public/manifest.json
public/robots.txt
README.md
server/.env.example
server/agents/accessibility.js
server/agents/ask.js
server/agents/climate.js
server/agents/coordinator.js
server/agents/housing.js
server/agents/urbanDesign.js
server/agents/vision.js
server/index.js
server/package.json
server/routes/analysis.js
server/routes/ask.js
server/routes/conditions.js
server/routes/health.js
server/routes/location.js
server/routes/upload.js
server/routes/visualize.js
server/scripts/compression_benchmark_ttc_longcontext.py
server/scripts/compression_benchmark_ttc.py
server/scripts/compression-bench.js
server/scripts/test-census.js
server/scripts/verify-accessibility-transit.js
server/scripts/verify-ask-grounding.js
server/scripts/verify-climate-fema.js
server/scripts/verify-climate-nlcd.js
server/scripts/verify-climate-openmeteo.js
server/scripts/verify-housing-census.js
server/scripts/verify-vision-baselines.js
server/services/accessibilityAgentParser.js
server/services/censusService.js
server/services/claudeService.js
server/services/climateAgentParser.js
server/services/conditionsService.js
server/services/femaNfhlService.js
server/services/googleMapsService.js
server/services/housingAgentParser.js
server/services/midjourneyMcpClient.js
server/services/midjourneyService.js
server/services/nlcdTccService.js
server/services/openMeteoService.js
server/services/promptCompression.js
server/services/renderingProvider.js
server/services/transit511Service.js
src/App.js
src/components/AgentCard.js
src/components/AIAssistantPanel.js
src/components/analysis/AgentPipeline.js
src/components/analysis/AgentRow.js
src/components/AnalysisStatusBar.js
src/components/ConditionsBar.js
src/components/ControlStrip.js
src/components/CurrentConditionsPanel.js
src/components/DataMethodologySection.js
src/components/insights/RecommendationCard.js
src/components/insights/RiskItem.js
src/components/insights/RisksPanel.js
src/components/InterventionCard.js
src/components/interventions/InterventionCard.js
src/components/interventions/interventionImages.js
src/components/interventions/InterventionsStrip.js
src/components/InterventionsPanel.js
src/components/layout/AppShell.js
src/components/layout/TopHeader.js
src/components/LocationSearch.js
src/components/MainMapPanel.js
src/components/map/RecenterMap.js
src/components/PlanningFindings.js
src/components/PresentDayView.js
src/components/ProjectedScenarioChanges.js
src/components/ReadyToAnalyzeCard.js
src/components/RecommendationsPanel.js
src/components/ReferenceImageInput.js
src/components/RisksPanel.js
src/components/scenarios/ScenarioCard.js
src/components/ScoreBreakdownPanel.js
src/components/shared/ConditionsBar.js
src/components/shared/DemoBadge.js
src/components/shared/ScoreRing.js
src/components/shared/StatBadge.js
src/components/StreetViewPanel.js
src/components/TopHeader.js
src/components/ui/badge.js
src/components/ui/card.js
src/components/ui/provenance-chip.js
src/components/ui/scroll-area.js
src/components/ui/severity-badge.js
src/components/ui/tabs.js
src/components/ui/tooltip.js
src/components/VisualizeStreetscapeAction.js
src/constants/planning.js
src/index.css
src/index.js
src/lib/utils.js
src/services/analysisApi.js
src/setupProxy.js
src/utils/formatters.js
src/utils/planningHelpers.js
tailwind.config.js
```

### Dependencies

- package.json: @googlemaps/js-api-loader@^2.1.1, @radix-ui/react-scroll-area@^1.2.12, @radix-ui/react-slot@^1.3.0, @radix-ui/react-tabs@^1.1.15, @radix-ui/react-tooltip@^1.2.10, @sentry/react@^10.59.0, @testing-library/dom@^10.4.1, @testing-library/jest-dom@^6.8.0, @testing-library/react@^16.3.0, @testing-library/user-event@^13.5.0, autoprefixer@^10.4.21, class-variance-authority@^0.7.1, clsx@^2.1.1, leaflet@^1.9.4, lucide-react@^0.544.0, postcss@^8.5.6, react@^19.1.1, react-dom@^19.1.1, react-leaflet@^5.0.0, react-scripts@5.0.1, tailwind-merge@^3.6.0, tailwindcss@^3.4.17, tailwindcss-animate@^1.0.7, web-vitals@^2.1.4
- server/package.json: @anthropic-ai/sdk@^0.39.0, @modelcontextprotocol/sdk@^1.29.0, adm-zip@^0.5.17, cors@^2.8.5, dotenv@^16.4.5, express@^4.19.2, multer@^2.2.0, the-token-company@^0.3.2

### Recent commits (newest first)

- chore: remove unused Render deployment config
- docs: add agent pipeline diagram referenced from README
- docs: bring README architecture, env vars, and known limitations up to date
- fix: use verified FEMA data for Flood Risk badge, drop unverified Heat Risk
- fix: backfill results grid with grid-flow-dense to fix scattered layout
- docs: split token compression README section into benchmark/encoding/production
- chore: add TTC_API_KEY to Render blueprint
- perf: compress agent prompts with the-token-company, integrate TTC client
- refactor: split results workspace into a two-column grid, remove duplicate scenario selector
- fix: reduce exaggerated Midjourney prompts, add Render deploy config
- feat: add production logo, final polish, and pre-analysis onboarding
- Merge branch 'main' of https://github.com/KangJustin/urbanpilot
- refactor: redesign map and street view workspace
- refactor: reorganize planning findings and methodology
- refactor: add verified conditions and scenario performance
- refactor: redesign header and planning controls
- style: add civic design tokens and data-status primitives
- Improve Midjourney visualization prompt fidelity
- Remove bundled Berkeley mock data entirely
- Merge branch 'main' of https://github.com/KangJustin/urbanpilot

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

### design-system/MASTER.md

```markdown
# UrbanPilot Design System — Civic Planning Redesign (Master)

Branch: `redesign/civic-planning-ui`. Status: **Phase 1 complete** (additive tokens + primitives
only — no page composition, API, state, or behavior changes anywhere in this phase).

## Tokens

### Civic palette (new, `tailwind.config.js` → `theme.extend.colors.civic`)

| Token | Value | Use |
|---|---|---|
| `civic-bg` | `#F4F6F3` | App background |
| `civic-surface` | `#FFFFFF` | Primary cards/panels |
| `civic-surface-secondary` | `#F8FAF8` | Secondary surfaces, chip backgrounds |
| `civic-border` | `#DDE3DF` | All borders/dividers |
| `civic-text` | `#17201C` | Primary text |
| `civic-text-muted` | `#65706A` | Secondary text, labels |
| `civic-accent` | `#167A59` | Primary actions, Climate category, Verified |
| `civic-accessibility` | `#3975A8` | Accessibility category, geographic data |
| `civic-housing` | `#B7791F` | Housing category, modeled data |
| `civic-risk-low` | `#167A59` | Severity: Low |
| `civic-risk-moderate` | `#B7791F` | Severity: Moderate |
| `civic-risk-high` | `#C2410C` | Severity: High |
| `civic-risk-critical` | `#B91C1C` | Severity: Critical — **red reserved for this tier only** |

`shadow-civic-sm` (`0 1px 2px rgba(23,32,28,0.06)`) is the only new elevation step — no colored
shadows, no glow, no blur/glassmorphism anywhere in the civic palette.

### Legacy tokens (`tailwind.config.js` → `theme.extend.colors.up`) — unchanged, still live

**These are a separate namespace, not renamed, not removed, not aliased.** Full audit:

| Token | Live usage | Orphaned-tree-only usage |
|---|---|---|
| `up-navy` | `src/index.css:10` (global `body` background — **live on every page**) | `layout/AppShell.js` |
| `up-surface` | `src/index.css:21` (`.up-panel-solid` definition) | — |
| `up-border` | `src/index.css:21` (`.up-panel-solid` definition) | `layout/TopHeader.js` |
| `up-charcoal` | none | `layout/TopHeader.js`, `interventions/InterventionsStrip.js` |
| `up-accent` | none (Tailwind color) — `boxShadow.up-accent` separately unused anywhere | `layout/TopHeader.js` |
| `up-surface-raised`, `up-border-subtle`, `up-accent-muted`, `up-accent-glow` | none found | none found |
| `.up-panel`, `.up-card`, `.up-card-compact`, `.up-label`, `.up-heading`, `.up-btn-primary`, `.up-input`, `.up-live-badge` (custom classes, `index.css` `@layer components`) | defined in `index.css` but not used by any live component | `analysis/AgentPipeline.js`, `insights/RecommendationCard.js`, `interventions/InterventionCard.js`, `interventions/InterventionsStrip.js`, `shared/ConditionsBar.js` |

**Conclusion:** `up-navy`/`up-surface`/`up-border` cannot be removed or repurposed without a visible
change to the live global body background and the (currently-unused-but-defined) `.up-panel-solid`
class. No alias was needed in Phase 1 because nothing was renamed or removed — the civic palette
uses entirely distinct names. The orphaned `layout/`, `analysis/`, `insights/`, `interventions/`,
`shared/`, `ma
[truncated — 4869 more characters]
```

### package.json

```
{
  "name": "urban-planning-tool",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@googlemaps/js-api-loader": "^2.1.1",
    "@radix-ui/react-scroll-area": "^1.2.12",
    "@radix-ui/react-slot": "^1.3.0",
    "@radix-ui/react-tabs": "^1.1.15",
    "@radix-ui/react-tooltip": "^1.2.10",
    "@sentry/react": "^10.59.0",
    "@testing-library/dom": "^10.4.1",
    "@testing-library/jest-dom": "^6.8.0",
    "@testing-library/react": "^16.3.0",
    "@testing-library/user-event": "^13.5.0",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "leaflet": "^1.9.4",
    "lucide-react": "^0.544.0",
    "react": "^19.1.1",
    "react-dom": "^19.1.1",
    "react-leaflet": "^5.0.0",
    "react-scripts": "5.0.1",
    "tailwind-merge": "^3.6.0",
    "tailwindcss-animate": "^1.0.7",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "autoprefixer": "^10.4.21",
    "postcss": "^8.5.6",
    "tailwindcss": "^3.4.17"
  }
}

```

### server/package.json

```
{
  "name": "urbanpilot-server",
  "version": "0.1.0",
  "private": true,
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "node --watch index.js"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.39.0",
    "@modelcontextprotocol/sdk": "^1.29.0",
    "adm-zip": "^0.5.17",
    "cors": "^2.8.5",
    "dotenv": "^16.4.5",
    "express": "^4.19.2",
    "multer": "^2.2.0",
    "the-token-company": "^0.3.2"
  }
}

```

### index.js

```javascript
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
```

### App.js

```javascript
import React, { useState } from 'react';
import './index.css';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
import L from 'leaflet';

// Fix for default markers in react-leaflet
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
  iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
  iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
  shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});

// Simple icon components
const MapIcon = () => (
  <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
    <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
    <circle cx="12" cy="10" r="3"/>
  </svg>
);

const LayersIcon = () => (
  <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
    <polygon points="12,2 2,7 12,12 22,7 12,2"/>
    <polyline points="2,17 12,22 22,17"/>
    <polyline points="2,12 12,17 22,12"/>
  </svg>
);

const BarChartIcon = () => (
  <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
    <line x1="12" y1="20" x2="12" y2="10"/>
    <line x1="18" y1="20" x2="18" y2="4"/>
    <line x1="6" y1="20" x2="6" y2="16"/>
  </svg>
);

// Component to update map view based on active overlays
function MapUpdater({ activeOverlays, currentScenario }) {
  const map = useMap();
  
  // You can add logic here to update map layers based on activeOverlays
  // For now, we'll just log the changes
  React.useEffect(() => {
    console.log('Active overlays:', activeOverlays);
    console.log('Current scenario:', currentScenario);
  }, [activeOverlays, currentScenario]);
  
  return null;
}

function App() {
  const [currentScenario, setCurrentScenario] = useState('current');
  const [activeOverlays, setActiveOverlays] = useState(['heat', 'green']);

  const handleOverlayToggle = (overlayId) => {
    setActiveOverlays(prev => 
      prev.includes(overlayId)
        ? prev.filter(id => id !== overlayId)
        : [...prev, overlayId]
    );
  };

  // Scenario data
  const scenarios = [
    { 
      id: 'current', 
      label: 'Current State', 
      desc: 'Existing conditions',
      metrics: { green: 18, temp: 85, air: 'C+', energy: 65, walk: 42 }
    },
    { 
      id: 'basic', 
      label: 'Basic Development', 
      desc: 'Standard planning approach',
      metrics: { green: 23, temp: 83, air: 'B-', energy: 72, walk: 58 }
    },
    { 
      id: 'climate', 
      label: 'Climate-Responsive', 
      desc: 'Optimized for sustainability',
      metrics: { green: 31, temp: 78, air: 'B+', energy: 89, walk: 76 }
    }
  ];

  // Overlay options
  const overlayOptions = [
    { 
      id: 'heat', 
      label: 'Temperature', 
      desc: 'Urban heat island effect',
      color: 'bg-red-500' 
    },
    { 
      id: 'green', 
      label: 'Green Coverage', 
      desc: 'Vegetation and parks',
      color: 'bg-green-500' 
    },
    { 
      id: 'traffic', 
      label: 'Traffic Flow', 
      desc: 'Vehicle density patterns',
      color: 'bg-blue-500' 
    }
  ];

  // Get current scenario data
  const currentScenarioData = scenarios.find(s => s.id === currentScenario) || scenarios[0];

  // Sample data points for the map
  const mapMarkers = [
    {
      id: 1,
      position: [42.3601, -71.0589], // Boston coordinates
      title: "Climate Pavilion Alpha",
      type: "climate",
      efficiency: "high"
    },
    {
      id: 2,
      position: [42.3584, -71.0598],
      title: "Green Space Beta",
      type: "green",
      efficiency: "medium"
    },
    {
      id: 3,
      position: [42.3611, -71.0571],
      title: "Urban Development Gamma",
      type: "building",
      efficiency: "low"
    }
  ];

  // Debug: Log when component renders
  console.log('App component rendering, currentScenario:', currentScenario);

  return (
    <div className="h-screen flex bg-gray-50">
      {/* Left Sidebar */}
      <div className="w-80 bg-white shadow-lg overflow-y-auto">
        <div className="p-6 space-y-6">
          {/* Header */}
          <div>
            <h1 className="text-2xl font-bold text-gray-800 mb-2">
              Urban Planning Tool
            </h1>
            <p className="text-gray-600">
              Visualize climate-responsive development impacts
            </p>
          </div>

          {/* Scenario Selector */}
          <div>
            <h3 className="font-semibold text-gray-800 mb-4">Development Scenarios</h3>
            <div className="space-y-3">
              {scenarios.map(scenario => (
                <button
                  key={scenario.id}
                  onClick={() => setCurrentScenario(scenario.id)}
                  className={`w-full text-left p-3 rounded-lg border-2 transition-all ${
                    currentScenario === scenario.id
                      ? 'border-blue-500 bg-blue-50'
                      : 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
                  }`}
                >
                  <div className="font-medium text-gray-800">{scenario.label}</div>
                  <div className="text-sm text-gray-600">{scenario.desc}</div>
                </button>
              ))}
            </div>
          </div>

          {/* Overlay Controls */}
          <div>
            <h3 className="font-semibold text-gray-800 mb-4 flex items-center">
              <LayersIcon />
              <span className="ml-2">Map Overlays</span>
            </h3>
            <div className="space-y-3">
              {overlayOptions.map(overlay => (
                <div key={overlay.id} className="flex items-center justify-between">
                  <div className="flex items-center space-x-3">
                    <div className={`w-3 h-3 rounded-full ${overlay.color}`}></div>
                    <div>
                     
[truncated — 8099 more characters]
```

### src/index.js

```javascript
import React from 'react';
import ReactDOM from 'react-dom/client';
import * as Sentry from '@sentry/react';
import './index.css';
import App from './App';
import 'leaflet/dist/leaflet.css';

Sentry.init({
  dsn: 'https://88a79ec1bcc225ef2e5aebdc5f92cd14@o4511601777836032.ingest.us.sentry.io/4511601791729664',
  dataCollection: {
    // To disable sending user data and HTTP bodies, uncomment the lines below. For more info visit:
    // https://docs.sentry.io/platforms/javascript/guides/react/configuration/options/#dataCollection
    // userInfo: false,
    // httpBodies: []
  },
  integrations: [
    Sentry.browserTracingIntegration(),
    Sentry.replayIntegration(),
  ],
  // Tracing
  tracesSampleRate: 1.0, // Capture 100% of the transactions
  // Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
  tracePropagationTargets: ['localhost', /^\/api\//],
  // Session Replay
  replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
  replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
  // Enable logs to be sent to Sentry
  enableLogs: true,
});

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
```

### server/index.js

```javascript
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const healthRouter = require('./routes/health');
const analysisRouter = require('./routes/analysis');
const visualizeRouter = require('./routes/visualize');
const conditionsRouter = require('./routes/conditions');
const askRouter = require('./routes/ask');
const locationRouter = require('./routes/location');
const uploadRouter = require('./routes/upload');

const app = express();
const PORT = process.env.PORT || 3001;

const allowedOrigins = ['http://localhost:3000', ...(process.env.ALLOWED_ORIGINS?.split(',') || [])];
app.use(cors({ origin: allowedOrigins }));
app.use(express.json());
app.use('/uploads', express.static(require('path').join(__dirname, 'uploads')));

app.use('/api', healthRouter);
app.use('/api', analysisRouter);
app.use('/api', visualizeRouter);
app.use('/api', conditionsRouter);
app.use('/api', askRouter);
app.use('/api', locationRouter);
app.use('/api', uploadRouter);

app.listen(PORT, () => {
  console.log(`UrbanPilot server running on http://localhost:${PORT}`);
  if (!process.env.ANTHROPIC_API_KEY) {
    console.warn('Warning: ANTHROPIC_API_KEY not set — agents will fall back to mock data');
  }
  if (!process.env.GOOGLE_MAPS_SERVER_API_KEY) {
    console.warn('Warning: GOOGLE_MAPS_SERVER_API_KEY not set — location search and present-day view will be unavailable');
  }
  if (!process.env.CENSUS_API_KEY) {
    console.warn('Warning: CENSUS_API_KEY not set — Housing Agent will run without verified ACS data');
  }
});

```

### src/App.js

```javascript
import React, { useState, useEffect } from 'react';
import {
  Sun, Cloud, CloudRain, CloudSnow, CloudLightning, CloudFog,
} from 'lucide-react';
import {
  analyzeNeighborhood, generateVisualization, getConditions, askQuestion,
  getStreetViewStatus, streetViewImageUrl, satelliteImageUrl,
} from './services/analysisApi';
import StreetViewPanel from './components/StreetViewPanel';
import TopHeader from './components/TopHeader';
import ControlStrip from './components/ControlStrip';
import DataMethodologySection from './components/DataMethodologySection';
import AnalysisStatusBar from './components/AnalysisStatusBar';
import ScoreBreakdownPanel from './components/ScoreBreakdownPanel';
import CurrentConditionsPanel from './components/CurrentConditionsPanel';
import PlanningFindings from './components/PlanningFindings';
import MainMapPanel from './components/MainMapPanel';
import AIAssistantPanel from './components/AIAssistantPanel';
import ReadyToAnalyzeCard from './components/ReadyToAnalyzeCard';
import ProjectedScenarioChanges from './components/ProjectedScenarioChanges';
import VisualizeStreetscapeAction from './components/VisualizeStreetscapeAction';

// Placeholder shown before the user searches for a location. Never re-substituted after a
// real place is selected — see selectedLocation state in App().
const DEFAULT_LOCATION = {
  placeId: null,
  displayName: 'Downtown Berkeley, CA',
  formattedAddress: 'Downtown Berkeley, CA',
  latitude: 37.8703,
  longitude: -122.2677,
};

const AGENTS = [
  { id: 'coordinator', label: 'Coordinator' },
  { id: 'climate', label: 'Climate Agent' },
  { id: 'accessibility', label: 'Accessibility Agent' },
  { id: 'housing', label: 'Housing Agent' },
  { id: 'urban_design', label: 'Urban Design Agent' },
];

const GOAL_CHIPS = [
  'Add housing near transit',
  'Reduce urban heat island effect',
  'Improve bike & pedestrian safety',
  'Increase tree canopy coverage',
  'Add affordable housing',
  'Improve flood resilience',
];


// WMO weather codes -> icon, per https://open-meteo.com/en/docs
function weatherIcon(code) {
  if (code === 0) return Sun;
  if ([1, 2, 3].includes(code)) return Cloud;
  if ([45, 48].includes(code)) return CloudFog;
  if (code >= 51 && code <= 67) return CloudRain;
  if (code >= 71 && code <= 77) return CloudSnow;
  if (code >= 80 && code <= 99) return CloudLightning;
  return Cloud;
}

function aqiCategory(aqi) {
  if (aqi == null) return null;
  if (aqi <= 50) return { label: 'Good', color: 'text-emerald-400' };
  if (aqi <= 100) return { label: 'Moderate', color: 'text-amber-400' };
  if (aqi <= 150) return { label: 'Unhealthy (SG)', color: 'text-orange-400' };
  if (aqi <= 200) return { label: 'Unhealthy', color: 'text-rose-400' };
  return { label: 'Very Unhealthy', color: 'text-purple-400' };
}

// FEMA NFHL-derived, not AI — femaFloodRisk is a deterministic lookup over the verified
// flood zone/SFHA fields (server/services/femaNfhlService.js: floodRiskFromZone()). No
// verified heat-risk dataset exists anywhere in this codebase (heat island is Claude
// narrative only), so there's no equivalent getHeatRisk — the header no longer shows one
// rather than label an AI guess as a risk badge.
function getFloodRisk(climate) {
  const fema = climate?.climateData;
  if (!climate?.climateAvailable || fema?.femaFloodRisk == null) return null;
  return { label: fema.femaFloodRisk };
}

export default function App() {
  const [goal, setGoal] = useState('');
  const [analysisState, setAnalysisState] = useState('idle');
  const [agentStatuses, setAgentStatuses] = useState({});
  const [results, setResults] = useState(null);
  const [copied, setCopied] = useState(false);
  const [visualizingYear, setVisualizingYear] = useState(null);
  const [visualizedImages, setVisualizedImages] = useState({});
  const [visualizeError, setVisualizeError] = useState(null);
  const [conditions, setConditions] = useState(null);
  const [selectedScenario, setSelectedScenario] = useState('2040');
  const [chatMessages, setChatMessages] = useState([]);
  const [chatInput, setChatInput] = useState('');
  const [chatLoading, setChatLoading] = useState(false);
  const [selectedLocation, setSelectedLocation] = useState(DEFAULT_LOCATION);
  const [analysisError, setAnalysisError] = useState(null);
  const [userVisionText, setUserVisionText] = useState('');
  const [referenceImage, setReferenceImage] = useState(null);
  const [presentPhotoUrl, setPresentPhotoUrl] = useState(null);
  const [presentPhotoSource, setPresentPhotoSource] = useState(null);

  useEffect(() => {
    let cancelled = false;
    getConditions(selectedLocation.latitude, selectedLocation.longitude)
      .then(c => { if (!cancelled) setConditions(c); })
      .catch(() => { if (!cancelled) setConditions(null); });
    return () => { cancelled = true; };
  }, [selectedLocation]);

  // The real "now" photo for the site — Street View if covered, satellite otherwise. Used to
  // display the 2026 scenario, and (per explicit product decision, accepting the associated
  // Google Maps Platform ToS risk) as the default Midjourney reference image for 2040/2075
  // when the user hasn't uploaded their own photo.
  useEffect(() => {
    let cancelled = false;
    const { latitude, longitude } = selectedLocation;
    getStreetViewStatus(latitude, longitude)
      .then(({ available }) => {
        if (cancelled) return;
        setPresentPhotoUrl(available ? streetViewImageUrl(latitude, longitude) : satelliteImageUrl(latitude, longitude));
        setPresentPhotoSource(available ? 'google-street-view' : 'google-satellite');
      })
      .catch(() => {
        if (cancelled) return;
        setPresentPhotoUrl(satelliteImageUrl(latitude, longitude));
        setPresentPhotoSource('google-satellite');
      });
    return () => { cancelled = true; };
  }, [selectedLocation]);

  function handleLocationSelected(location) {
    setSelectedLocation(location);
    // A new location invalidates any analy
[truncated — 13294 more characters]
```

### postcss.config.js

```javascript
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

```

### index.css

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Import Leaflet CSS */
@import 'leaflet/dist/leaflet.css';

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Ensure map container has proper height */
.leaflet-container {
  height: 100%;
  width: 100%;
}
```

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