QuipuEco’s UI is composed of five self-contained React components orchestrated by a singleDocumentation 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.
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 howApp.jsx wires all five components together. Every prop referenced in the individual sections below appears here.
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
Called when the
/clasificar API request completes successfully.data— the full classification JSON object returned by the endpoint (containsnombre,tipo,co2_evitado_kg,peso_estimado_kg,instrucciones,punto_acopio,consejo,respuesta_voz)preview— theblob:URL string created from the selected or captured image file, suitable for use as an<img src>value
App.jsx, handleResult calls agregarRegistro(data, preview), sets resultado and imagen state, then switches vista to "resultado".Internal state
Internal state reference
Internal state reference
| State variable | Type | Description |
|---|---|---|
tab | "upload" | "camera" | Active input tab |
preview | string | null | Current blob URL shown in the preview area |
file | File | null | The File object selected or captured |
loading | boolean | true while the POST to /clasificar is in-flight; shows a scan-line overlay |
error | string | null | Error message shown in the red alert block |
cameraActive | boolean | Whether the getUserMedia stream is running |
Usage example
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-placeAgenteVoz overlay when agenteAbierto is true, controlled by the parent through the agent-related props.
Props
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.Blob URL of the classified image, displayed as the card’s hero image. Comes from the
preview argument of ImageCapture’s onResult callback.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.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.Controls whether the
AgenteVoz overlay is rendered on top of the result card. Managed in App.jsx via setAgenteAbierto.Called when the user taps the Hablar con el Agente (or Continuar con el Agente) button. Sets
agenteAbierto to true in App.jsx.Passed through to
AgenteVoz as its onCerrar prop. Sets agenteAbierto to false in App.jsx.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).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
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
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.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 value | data | puntoCercano | Effect in App.jsx |
|---|---|---|---|
"mapa" | Material type slug from accion_data (e.g. "electronico") | Nearest collection point object from punto_cercano, or null | Calls handleVerMapa(resultado, puntoCercano ?? data) |
"dashboard" | null | null | Sets vista to "dashboard" |
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.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.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 inhistorialInicial and the array passed to onHistorialChange follows this structure:
Usage example
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-framediv 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
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.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.Controls automatic filtering and routing behavior:
In
| Value type | Behavior |
|---|---|
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. |
null | No override—category is derived from resultado.tipo and no auto-route is triggered. |
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
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
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.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.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 mapsstats.puntos to one of five display tiers:
| Points threshold | Label | Emoji |
|---|---|---|
| 0 | Semilla Eco | 🌱 |
| 30 | Árbol Joven | 🌳 |
| 100 | Bosque Verde | 🌲 |
| 250 | Guardián Eco | 🦅 |
| 500 | Héroe del Planeta | 🏆 |
Usage example
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.