Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/davidmpizarro/QuipuEco-Hackaton/llms.txt

Use this file to discover all available pages before exploring further.

QuipuEco follows a clean two-tier architecture: a stateful React SPA handles all user interaction, routing, and media capture in the browser, while a thin FastAPI service owns all AI inference and collection-point data. There is no shared database, no authentication layer, and no WebSocket connection — every interaction is a discrete HTTP POST that resolves to a JSON payload the frontend immediately renders. This keeps the system auditable, easy to deploy, and straightforward to extend with new waste categories or collection networks.

System Data Flow

User

  │  Photo upload / voice input

React Frontend (Vite SPA — localhost:5173)

  │  POST /clasificar   — multipart/form-data image
  │  POST /chat         — JSON { mensaje, historial, contexto_residuo, lat, lng }

FastAPI Backend (Python — localhost:8000)

  │  image bytes + classification prompt
  │  OR conversational prompt + residue context

Google Gemini API (gemini-3.1-flash-lite)

  │  JSON classification response
  │  OR conversational reply + optional action intent

FastAPI Backend
  │  Haversine nearest-point calculation (for /chat responses)
  │  Pydantic validation and serialisation

React Frontend

  ├─► UI render (ClassificationResult / AgenteVoz chat bubble)
  ├─► Web Speech API SpeechSynthesis (TTS audio playback)
  └─► Mapbox GL JS (VistaMapaPuntos fullscreen map + route)

Frontend Architecture

The entire frontend lives in a single App.jsx component that uses React’s useState to drive routing between four named views. There is no React Router — navigation is purely state-based.

State-Driven Routing

The vista state variable is the single source of truth for which view is rendered:
const [vista, setVista] = useState("captura");
// Possible values: "captura" | "resultado" | "mapa" | "dashboard"
vista valueComponent renderedTriggered by
"captura"<ImageCapture>App load, reset button, Clasificar nav tab
"resultado"<ClassificationResult>Successful /clasificar API response
"mapa"<VistaMapaPuntos> (fullscreen overlay)“Ver mapa” button, voice agent mapa action
"dashboard"<ImpactDashboard>Mi Impacto nav tab, voice agent dashboard action
The voice agent (AgenteVoz) is rendered as an absolute-positioned overlay on top of the active view. It is controlled by a separate agenteAbierto boolean rather than the vista state, allowing it to float over both the "resultado" and "captura" views simultaneously.

Component Responsibilities

File: src/components/ImageCapture.jsxHandles the two image input modes — file upload (with drag-and-drop) and live camera capture. When a file is selected or a frame is captured from the getUserMedia video stream, the component renders a preview and exposes a Clasificar Residuo button.On submit, it constructs a FormData object and calls POST http://localhost:8000/clasificar. On success, it passes the API response and the image preview URL to the onResult callback in App.jsx, which transitions vista to "resultado".
const clasificar = async () => {
  const formData = new FormData();
  formData.append("file", file);
  const { data } = await axios.post(`${API_URL}/clasificar`, formData, {
    headers: { "Content-Type": "multipart/form-data" },
  });
  onResult(data, preview);
};

The useHistorial Hook

File: src/hooks/useHistorial.js This custom hook provides localStorage persistence for all classification records. It is the only form of data persistence in the application — there is no user account, no server-side database, and no session storage.
// Storage key used in localStorage
const STORAGE_KEY = "quipueco_historial";

// Each record saved to history
const nuevo = {
  id: Date.now(),
  fecha: new Date().toISOString(),
  imagen,        // base64 or object URL of the captured photo
  ...resultado,  // full spread of the /clasificar API response
};

// Hook return shape
return { historial, agregarRegistro, limpiarHistorial, stats };
The stats object is computed on every render as a derived value from historial:
const stats = {
  totalItems: historial.length,
  co2Total:   historial.reduce((acc, r) => acc + (r.co2_evitado_kg || 0), 0),
  pesoTotal:  historial.reduce((acc, r) => acc + (r.peso_estimado_kg || 0), 0),
  porTipo:    historial.reduce((acc, r) => { acc[r.tipo] = (acc[r.tipo] || 0) + 1; return acc; }, {}),
  puntos:     historial.reduce((acc, r) => {
    if (r.tipo === "no_reciclable") return acc + 2;
    if (r.tipo === "peligroso")     return acc + 5;
    return acc + 10;
  }, 0),
};

GSAP Animations

On desktop viewports, App.jsx uses GSAP to animate the hero mascot, title, subtitle, feature cards, and footer on first render. All animations are scoped with gsap.context() and reverted on unmount. The mascot also has interactive hover (wiggle) and click (360° spin) animations. These animations are guarded by a ref check — if the mascot element does not exist in the DOM (i.e., on mobile), no animation is attempted.

Backend Architecture

API Endpoints

The FastAPI backend exposes two POST endpoints. Both use the gemini-3.1-flash-lite model, accessed via the Google Generative AI Python SDK.
Accepts a multipart form upload with a single file field containing an image (JPEG, PNG, WEBP, max ~10 MB in practice).The endpoint:
  1. Reads the image bytes and encodes them for the Gemini multimodal API
  2. Sends the image with a structured classification prompt from prompts.py
  3. Parses the Gemini response into the Pydantic response model (models.py)
  4. Returns a JSON object with all fields needed by ClassificationResult.jsx
Request:
POST /clasificar
Content-Type: multipart/form-data

file: <image bytes>
Response fields:
FieldTypeDescription
nombrestringHuman-readable item name in Spanish
tipostringMaterial category (plastico, papel, vidrio, organico, metal, electronico, peligroso, no_reciclable)
instruccionesstringPreparation instructions for Lima collection points
punto_acopiostringCollection network name (Tiendas Tambo or Centro Verde Municipal)
co2_evitado_kgfloatEstimated CO₂ savings in kilograms
peso_estimado_kgfloatEstimated item weight in kilograms
consejostringEco tip related to the classified material
respuesta_vozstringShort TTS-optimised announcement for the voice agent

Haversine Distance Calculation

The backend’s utils.py implements the Haversine formula to compute great-circle distances between the user’s GPS coordinates and every collection point in data/puntos.py. The same formula is also implemented in the frontend’s VistaMapaPuntos.jsx to sort the visible card list by proximity after geolocation is acquired.
import math

def haversine(lat1, lng1, lat2, lng2):
    R = 6371  # Earth radius in km
    d_lat = math.radians(lat2 - lat1)
    d_lng = math.radians(lng2 - lng1)
    a = (math.sin(d_lat / 2) ** 2
         + math.cos(math.radians(lat1))
         * math.cos(math.radians(lat2))
         * math.sin(d_lng / 2) ** 2)
    return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))

Collection Point Data (data/puntos.py)

All collection point coordinates are stored as static Python data structures in data/puntos.py. The data is partitioned by waste category, and the plastico, papel, and metal categories all point to the same TAMBO_REAL_LIMA_ESTE list. This partitioning is enforced both in the backend (for nearest-point calculation) and in the frontend VistaMapaPuntos.jsx (for map rendering).

The Two Collection Networks

The category-to-network routing is a hard constraint in both frontend and backend code, not a runtime preference.
Categories: plastico, papel, metalTambo convenience stores accept dry, clean recyclables that are safe to handle in a retail environment. No appointment is required — items are handed directly to store staff at the counter.The frontend uses the Tambo logo image as a custom Mapbox marker icon for these categories. The RED_POR_CATEGORIA mapping in VistaMapaPuntos.jsx controls which badge and marker style is displayed.Participating stores (Lima Este pilot):
StoreDistrictCoordinates
Tambo CorregidorLa Molina-12.0815, -76.9398
Tambo AlondrasSanta Anita-12.0445, -76.9678
Tambo ParacasAte-12.0398, -76.9156
Tambo AyllónChaclacayo-11.9821, -76.7701
Tambo Riva AgüeroEl Agustino-12.0478, -76.9967
Tambo 28 de JulioChosica-11.9356, -76.6945

Environment Variables

QuipuEco requires two environment variables, one per service. Neither variable is committed to version control.
VariableServiceFileDescription
VITE_MAPBOX_TOKENFrontendquipueco-frontend/.envMapbox public access token. Exposed to the browser bundle via Vite’s import.meta.env. Used in VistaMapaPuntos.jsx for map tiles and the Directions API.
GEMINI_API_KEYBackendquipueco-backend/.envGoogle AI Studio API key. Read server-side only via config.py. Never sent to the browser.

Environment Variable Configuration

See the full environment variable reference, including how to obtain each credential, set up local .env files, and configure the app for production deployment.

Backend Project Structure

quipueco-backend/
├── app/
│   ├── main.py        # FastAPI app instance, endpoint definitions, CORS config
│   ├── prompts.py     # Gemini prompt templates for /clasificar and /chat
│   ├── models.py      # Pydantic request and response models
│   ├── utils.py       # Haversine distance helper
│   └── config.py      # Gemini client initialisation (reads GEMINI_API_KEY)
└── data/
    └── puntos.py      # Static collection point coordinates by category

Frontend Project Structure

quipueco-frontend/
├── src/
│   ├── App.jsx                        # Root component: state, routing, GSAP animations
│   ├── components/
│   │   ├── ImageCapture.jsx           # File upload + live camera capture + /clasificar call
│   │   ├── ClassificationResult.jsx   # Classification card with material, CO₂, instructions
│   │   ├── AgenteVoz.jsx              # Voice agent overlay (Web Speech API + /chat)
│   │   ├── VistaMapaPuntos.jsx        # Fullscreen Mapbox map with routes
│   │   ├── ImpactDashboard.jsx        # Gamified impact stats and history
│   │   └── SplashScreen.jsx          # App loading screen
│   ├── hooks/
│   │   └── useHistorial.js            # localStorage persistence + stats computation
│   └── main.jsx                       # React DOM render entry point
├── public/
│   ├── images/                        # Mascot, Tambo logo, and store photos
│   └── videos/                        # Demo video for the landing section
├── index.html
└── vite.config.js

Build docs developers (and LLMs) love