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’s UI is composed of five self-contained React components orchestrated by a single vista state string in App.jsx. Each component manages its own internal state and communicates exclusively through the props listed here—there is no shared context or global store below the App level. App.jsx calls useHistorial once and fans the returned values out to whichever component needs them at any given moment.

App composition pattern

The following excerpt shows how App.jsx wires all five components together. Every prop referenced in the individual sections below appears here.
const { historial, agregarRegistro, limpiarHistorial, stats } = useHistorial();

// vista === "captura"
<ImageCapture onResult={handleResult} />

// vista === "resultado"
<ClassificationResult
  resultado={resultado}
  imagen={imagen}
  onReset={handleReset}
  onVerMapa={handleVerMapa}
  agenteAbierto={agenteAbierto}
  onAbrirAgente={() => setAgenteAbierto(true)}
  onCerrarAgente={() => setAgenteAbierto(false)}
  historialAgente={historialAgente}
  onHistorialAgente={setHistorialAgente}
/>

// vista === "dashboard"
<ImpactDashboard stats={stats} historial={historial} onLimpiar={limpiarHistorial} />

// vista === "mapa" (fullscreen, rendered outside the phone frame)
<VistaMapaPuntos
  resultado={resultadoMapa}
  onVolver={() => resultado ? setVista("resultado") : setVista("captura")}
  puntoDestino={puntoDestino}
/>

// Floating overlay (visible on "resultado" view when agenteAbierto is true)
<AgenteVoz
  resultado={resultado}
  onAccion={(accion, data, puntoCercano) => { /* navigate to map or dashboard */ }}
  onCerrar={() => setAgenteAbierto(false)}
  historialInicial={historialAgente}
  onHistorialChange={setHistorialAgente}
/>
All five components are fully self-contained—each manages its own internal state (tabs, loading flags, camera streams, map instances, speech recognition sessions, etc.) and surfaces nothing to the outside world except through the callback props listed below. Avoid reaching into component internals via refs.

ImageCapture

Presents a two-tab interface (file upload and live camera) for selecting or capturing a waste image. On submission it POSTs to the /clasificar endpoint and calls onResult with the parsed response and a blob preview URL. All loading state, error display, and getUserMedia lifecycle management are handled internally.

Props

onResult
(data: object, preview: string) => void
required
Called when the /clasificar API request completes successfully.
  • data — the full classification JSON object returned by the endpoint (contains nombre, tipo, co2_evitado_kg, peso_estimado_kg, instrucciones, punto_acopio, consejo, respuesta_voz)
  • preview — the blob: URL string created from the selected or captured image file, suitable for use as an <img src> value
In App.jsx, handleResult calls agregarRegistro(data, preview), sets resultado and imagen state, then switches vista to "resultado".

Internal state

State variableTypeDescription
tab"upload" | "camera"Active input tab
previewstring | nullCurrent blob URL shown in the preview area
fileFile | nullThe File object selected or captured
loadingbooleantrue while the POST to /clasificar is in-flight; shows a scan-line overlay
errorstring | nullError message shown in the red alert block
cameraActivebooleanWhether the getUserMedia stream is running

Usage example

import ImageCapture from "./components/ImageCapture";

function App() {
  const handleResult = (data, preview) => {
    console.log("Classified:", data.nombre, "→", data.tipo);
    console.log("Preview URL:", preview);
  };

  return <ImageCapture onResult={handleResult} />;
}
The camera tab requests { facingMode: "environment" } to prefer the rear-facing camera on mobile devices. On desktop browsers without a rear camera the default camera is used. If getUserMedia is denied, an error message is shown and the user is directed to upload a file instead.

ClassificationResult

Displays the full result card for a classified item: the image, material type badge, CO₂ and weight stats, disposal instructions, an eco tip, and a button to open the voice agent. It also renders an in-place AgenteVoz overlay when agenteAbierto is true, controlled by the parent through the agent-related props.

Props

resultado
object
required
The classification result object returned by /clasificar. Must contain at minimum tipo, nombre, co2_evitado_kg, peso_estimado_kg, instrucciones, punto_acopio, and consejo. The tipo key is used to look up the material’s color theme, emoji, and bin label from an internal config map.
imagen
string
required
Blob URL of the classified image, displayed as the card’s hero image. Comes from the preview argument of ImageCapture’s onResult callback.
onReset
() => void
required
Called when the user taps Clasificar otro residuo. In App.jsx, handleReset clears resultado, imagen, stops any active TTS, and navigates back to the "captura" vista.
onVerMapa
(resultado: object) => void
required
Called when the user taps the Puntos de acopio en Lima button. The component always passes the current resultado as the only argument. In App.jsx, handleVerMapa receives this and sets resultadoMapa before switching vista to "mapa". If you need to navigate to the map with a specific destination or category override, trigger handleVerMapa directly from the AgenteVoz.onAccion callback — ClassificationResult itself never supplies a second argument.
agenteAbierto
boolean
required
Controls whether the AgenteVoz overlay is rendered on top of the result card. Managed in App.jsx via setAgenteAbierto.
onAbrirAgente
() => void
required
Called when the user taps the Hablar con el Agente (or Continuar con el Agente) button. Sets agenteAbierto to true in App.jsx.
onCerrarAgente
() => void
required
Passed through to AgenteVoz as its onCerrar prop. Sets agenteAbierto to false in App.jsx.
historialAgente
array
required
The current conversation history for the embedded AgenteVoz instance. Passed down so the agent can restore its message list when it is re-opened after being closed. Stored in App.jsx as historialAgente state (initialized to [] on each new classification).
onHistorialAgente
(historial: array) => void
required
Setter passed to AgenteVoz as its onHistorialChange prop, so every new chat message updates the historialAgente state in App.jsx and survives the agent being toggled open/closed.

Usage example

<ClassificationResult
  resultado={resultado}
  imagen={imagen}
  onReset={() => {
    setResultado(null);
    setImagen(null);
    setVista("captura");
  }}
  onVerMapa={(r) => {
    setResultadoMapa(r);
    setPuntoDestino(null);
    setVista("mapa");
  }}
  agenteAbierto={agenteAbierto}
  onAbrirAgente={() => setAgenteAbierto(true)}
  onCerrarAgente={() => setAgenteAbierto(false)}
  historialAgente={historialAgente}
  onHistorialAgente={setHistorialAgente}
/>

AgenteVoz

A full-screen conversational overlay that provides a voice-first (and text-fallback) interface to a Gemini-backed /chat endpoint. On mount it speaks a greeting using the Web Speech API, then enters a continuous listen–respond loop. The agent can trigger app-level navigation to the map or dashboard by emitting structured onAccion callbacks.

Props

resultado
object | null
required
The current classification result, or null if no item has been classified yet. Sent as contexto_residuo in every POST to /chat, giving the LLM full context about the item being discussed. When null, the agent introduces itself as a general Lima recycling assistant.
onAccion
(accion: string, data: any, puntoCercano: any) => void
required
Called 1.8 seconds after the /chat response includes an accion field, giving the TTS engine time to finish speaking before the navigation happens.
accion valuedatapuntoCercanoEffect in App.jsx
"mapa"Material type slug from accion_data (e.g. "electronico")Nearest collection point object from punto_cercano, or nullCalls handleVerMapa(resultado, puntoCercano ?? data)
"dashboard"nullnullSets vista to "dashboard"
onCerrar
() => void
required
Called when the user taps the close button (✕) in the agent header. The component cancels any active speech synthesis and aborts the SpeechRecognition session before invoking this callback.
historialInicial
array
default:"[]"
An array of { rol: "usuario" | "agente", texto: string } objects used to pre-populate the chat history on mount. When non-empty, the opening greeting is skipped—the agent resumes mid-conversation without re-reading the initial message. Populated from historialAgente state in App.jsx.
onHistorialChange
(historial: array) => void
Called after every new message (both user and agent turns) with the complete updated history array. Used by App.jsx to keep historialAgente in sync so conversation state persists when the agent overlay is toggled.

Message object shape

Each entry in historialInicial and the array passed to onHistorialChange follows this structure:
{ "rol": "agente", "texto": "He clasificado una botella de plástico PET. ¿Quieres saber dónde llevarla?" }
{ "rol": "usuario", "texto": "Sí, ¿dónde la llevo?" }

Usage example

<AgenteVoz
  resultado={resultado}
  onAccion={(accion, data, puntoCercano) => {
    if (accion === "mapa") {
      handleVerMapa(resultado, puntoCercano ?? data);
    }
    if (accion === "dashboard") {
      setVista("dashboard");
    }
  }}
  onCerrar={() => setAgenteAbierto(false)}
  historialInicial={historialAgente}
  onHistorialChange={setHistorialAgente}
/>
The voice recognition session uses window.SpeechRecognition (or window.webkitSpeechRecognition on Chrome). The agent falls back gracefully to a text-only display if the browser does not support speech recognition, surfacing an error message in the chat. For best results use Chrome on Android or desktop—Safari’s speech recognition support is inconsistent.

VistaMapaPuntos

A full-viewport Mapbox GL JS map showing categorized recycling collection points across Lima Este. It renders outside the phone-frame div in App.jsx (fixed to the full screen via position: fixed; inset: 0). On load it triggers geolocation, sorts the collection point cards by distance, and can trace a walking route to any selected point via the Mapbox Directions API.

Props

resultado
object
required
The classification result used to resolve which collection network and category to display. The component reads resultado.tipo to select the correct marker color, emoji, and point list. Plastic, paper, and metal map to the Tambo store network; organic, glass, electronic, and hazardous materials map to Centro Verde Municipal points.
onVolver
() => void
required
Called when the user taps the back arrow (←) in the floating header. In App.jsx, this navigates to "resultado" if a classification is active, or to "captura" if the map was opened as a standalone tab.
puntoDestino
string | { nombre: string } | null
default:"null"
Controls automatic filtering and routing behavior:
Value typeBehavior
string (e.g. "electronico")Used as filtroForzado—overrides resultado.tipo for category detection. The map shows points for this category instead. No automatic route is traced; the user selects a card.
{ nombre: string }Treated as a specific destination. After geolocation resolves, the component finds the point whose name best matches nombre (fuzzy substring match) and automatically traces a walking route to it.
nullNo override—category is derived from resultado.tipo and no auto-route is triggered.
In App.jsx, puntoDestino is set to a { nombre, ts } object (with a timestamp to force re-triggering the auto-route effect) when the voice agent provides a puntoCercano value.

Collection point categories

Tambo Network

Covers plastico, papel, and metal. These materials are accepted at the counter of any participating Tambo convenience store in Lima Este. Six real locations are mapped.

Centro Verde Municipal

Covers organico, vidrio, electronico, peligroso, and general. These require specialist handling not available at a convenience store.

Usage example

<VistaMapaPuntos
  resultado={{ tipo: "plastico", nombre: "Botella PET" }}
  onVolver={() => setVista("resultado")}
  puntoDestino={null}
/>

// With a category filter from the voice agent:
<VistaMapaPuntos
  resultado={resultado}
  onVolver={() => setVista("resultado")}
  puntoDestino="electronico"
/>

// With a specific point for auto-routing:
<VistaMapaPuntos
  resultado={resultado}
  onVolver={() => setVista("resultado")}
  puntoDestino={{ nombre: "Tambo Corregidor", ts: Date.now() }}
/>
The ts field added to { nombre } objects in App.jsx ensures the auto-route useEffect re-fires even when the same point name is passed twice in a row, since React’s dependency comparison would otherwise consider the object unchanged.

ImpactDashboard

A stats dashboard that visualizes a user’s cumulative recycling impact. It renders three top-level metric tiles (CO₂ avoided, total weight, item count), an eco-rank progress card, and a three-tab panel containing a breakdown by material type, a Recharts cumulative CO₂ area chart, and a scrollable history list with per-item thumbnails.

Props

stats
object
required
The stats object from useHistorial(). Must contain totalItems, co2Total, pesoTotal, porTipo, and puntos. When totalItems is 0, the dashboard renders an empty-state prompt instead of the full layout.See the useHistorial reference for the complete stats field breakdown.
historial
array
required
The historial array from useHistorial(). Used in two places: the Recharts area chart reduces over a reversed copy to build a cumulative CO₂ time series, and the history tab lists the ten most recent records with their blob image thumbnails.
onLimpiar
() => void
required
Called when the user confirms deletion by tapping Limpiar Historial in the history tab. Should be wired directly to limpiarHistorial from useHistorial().

Eco-rank tiers

The dashboard maps stats.puntos to one of five display tiers:
Points thresholdLabelEmoji
0Semilla Eco🌱
30Árbol Joven🌳
100Bosque Verde🌲
250Guardián Eco🦅
500Héroe del Planeta🏆
An animated progress bar shows progress toward the next tier. At the maximum tier (Héroe del Planeta) the bar is replaced with a trophy banner.

Usage example

import ImpactDashboard from "./components/ImpactDashboard";
import { useHistorial } from "./hooks/useHistorial";

function App() {
  const { historial, limpiarHistorial, stats } = useHistorial();

  return (
    <ImpactDashboard
      stats={stats}
      historial={historial}
      onLimpiar={limpiarHistorial}
    />
  );
}
The history tab renders up to the ten most recent records. If more than ten records exist, a footer note displays the count of additional records stored in localStorage. All records remain accessible through the historial array; the ten-item cap is a UI concern only, not a data truncation.

Build docs developers (and LLMs) love