Skip to content

Commit 4f0faa0

Browse files
committed
feat: Add Ingestion page and routing
Introduces a new Ingestion page component for uploading sensor data to Edge Impulse. Adds routing for the new page and integrates it into the sidebar navigation. This facilitates the data flow from physical sensors to the AI model training pipeline.
1 parent bf9f19e commit 4f0faa0

File tree

4 files changed

+219
-64
lines changed

4 files changed

+219
-64
lines changed

App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { HashRouter, Routes, Route, Navigate } from 'react-router-dom';
44
import LoginPage from './components/LoginPage';
55
import DashboardPage from './components/DashboardPage';
66
import AnalysisPage from './components/AnalysisPage';
7+
import IngestionPage from './components/IngestionPage';
78
import CopilotWidget from './components/CopilotWidget';
89
import { VisionEdgeIcon } from './components/icons';
910

@@ -17,6 +18,7 @@ function App() {
1718
<Route path="/login" element={<LoginPage />} />
1819
<Route path="/dashboard" element={<DashboardPage />} />
1920
<Route path="/analysis" element={<AnalysisPage />} />
21+
<Route path="/ingestion" element={<IngestionPage />} />
2022
<Route path="*" element={<Navigate to="/login" />} />
2123
</Routes>
2224

README.md

Lines changed: 13 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,20 @@
1-
# Airguard VisionEdge
1+
<div align="center">
2+
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
3+
</div>
24

3-
**Airguard VisionEdge** is a concept for an advanced system that enables environmental researchers and on-site analysts to detect, visualize, and interpret greenhouse gas (GHG) anomalies in near-real-time using a fusion of Edge AI and satellite data.
5+
# Run and deploy your AI Studio app
46

5-
## 🌍 Concept Summary
7+
This contains everything you need to run your app locally.
68

7-
**Purpose:** To enable environmental researchers and on-site analysts to detect, visualize, and interpret greenhouse gas (GHG) anomalies in near-real-time using Edge AI + satellite fusion.
9+
View your app in AI Studio: https://ai.studio/apps/drive/1v-6gmO3NbzaoIYMnQoFR10ib-7kjQWrO
810

9-
## System Architecture
11+
## Run Locally
1012

11-
The system is designed with three core layers:
13+
**Prerequisites:** Node.js
1214

13-
1. **Edge Impulse Node:**
14-
* Runs optimized Machine Learning models (TinyML) for local emission pattern detection.
15-
* Performs inference on satellite raster tiles and ground sensor data (e.g., $CO_2$, $PM2.5$).
1615

17-
2. **Web Dashboard (Android-first):**
18-
* Displays fused insights through interactive maps, charts, and anomaly markers.
19-
* Syncs with Google Colab notebooks for advanced analytics and deep-dive visualization.
20-
21-
3. **LLM Assistant (VisionEdge Copilot):**
22-
* An on-device LLM assistant that explains observed trends.
23-
* Recommends research insights and provides context for data anomalies.
24-
25-
## 📱 UX Flow Overview
26-
27-
1. **Login & Device Sync Screen**
28-
* Users sign in via Google or their institutional account.
29-
* Sync connected Edge Impulse devices via Bluetooth/WiFi.
30-
* The "Add New Station" feature detects and registers a local AI node.
31-
32-
2. **Home Dashboard**
33-
* **Top Bar:** "VisionEdge" title with quick filters (Region | Model | Timeframe).
34-
* **Live Map Panel:** Displays raster data tiles with overlay layers for GHG, $NO_2$, and temperature. Edge inferences are highlighted as colored hotspots.
35-
* **Mini Stats Bar:** Shows key metrics like Emission Index, Confidence Level, and Anomaly Count. Tapping opens an expanded metrics view.
36-
37-
3. **Analysis Panel**
38-
* Organized into tabs: `AI Inference` | `Time Series` | `Correlations` | `Ground Data`.
39-
* Features interactive plots generated from Edge outputs.
40-
* An "Open in Colab" option launches a notebook session with linked data for deeper analysis.
41-
42-
4. **Copilot Assistant**
43-
* A floating chat widget allows users to "Ask VisionEdge Copilot."
44-
* Users can query the system with natural language, e.g., *“Explain today’s emission spike in the Cairo region”* to receive an AI-driven explanation.
45-
46-
5. **Export & Share**
47-
* Download comprehensive reports as PDF or GeoTIFF files.
48-
* Push results directly to a shared research group or an institutional drive.
49-
50-
## 🎨 Design Direction
51-
52-
* **Theme:** A space black background with green-cyan gradients to represent emission heatmaps.
53-
* **UI Style:** Sleek and minimal, following Material 3 design principles with a card-based layout.
54-
* **Interactions:** Smooth map transitions, animated data updates, and collapsible charts for a fluid user experience.
55-
* **Data Visualization:**
56-
* 2D raster overlays with opacity controls.
57-
* Dynamic graphs for comparing local inferences and historical data.
58-
59-
## Contributing
60-
61-
Contributions, issues, and feature requests are welcome.
62-
For significant contributions, please propose an issue first to discuss what you would like to change.
63-
64-
## License
65-
66-
Licensed under the MIT License. See `LICENSE` for details.
67-
68-
## Authors
69-
70-
* [Ahmed Ibrahim Metawee]
71-
* [AIMTY]
16+
1. Install dependencies:
17+
`npm install`
18+
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
19+
3. Run the app:
20+
`npm run dev`

components/IngestionPage.tsx

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
2+
import React, { useState, useCallback, useRef } from 'react';
3+
import Sidebar from './Sidebar';
4+
5+
const EDGE_IMPULSE_API_KEY = 'ei_238fae...'; // This should be securely stored, but for now it's here as requested.
6+
const EDGE_IMPULSE_INGESTION_API = 'https://ingestion.edgeimpulse.com/api/training/files';
7+
8+
const IngestionPage: React.FC = () => {
9+
const [files, setFiles] = useState<File[]>([]);
10+
const [label, setLabel] = useState('');
11+
const [status, setStatus] = useState<'idle' | 'uploading' | 'success' | 'error'>('idle');
12+
const [statusMessage, setStatusMessage] = useState('');
13+
const [isDragActive, setIsDragActive] = useState(false);
14+
const fileInputRef = useRef<HTMLInputElement>(null);
15+
16+
const onDrop = useCallback((acceptedFiles: File[]) => {
17+
setFiles(prevFiles => [...prevFiles, ...acceptedFiles]);
18+
}, []);
19+
20+
const handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
21+
e.preventDefault();
22+
e.stopPropagation();
23+
setIsDragActive(true);
24+
};
25+
26+
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
27+
e.preventDefault();
28+
e.stopPropagation();
29+
setIsDragActive(false);
30+
};
31+
32+
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
33+
e.preventDefault();
34+
e.stopPropagation();
35+
};
36+
37+
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
38+
e.preventDefault();
39+
e.stopPropagation();
40+
setIsDragActive(false);
41+
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
42+
onDrop(Array.from(e.dataTransfer.files));
43+
}
44+
};
45+
46+
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
47+
if (e.target.files && e.target.files.length > 0) {
48+
onDrop(Array.from(e.target.files));
49+
// Reset file input to allow selecting the same file again
50+
e.target.value = '';
51+
}
52+
};
53+
54+
const openFileDialog = () => {
55+
fileInputRef.current?.click();
56+
};
57+
58+
const removeFile = (fileName: string) => {
59+
setFiles(files.filter(file => file.name !== fileName));
60+
};
61+
62+
const handleUpload = async () => {
63+
if (files.length === 0 || !label.trim()) {
64+
setStatus('error');
65+
setStatusMessage('Please select files and provide a label.');
66+
return;
67+
}
68+
69+
setStatus('uploading');
70+
setStatusMessage('Uploading files...');
71+
72+
const formData = new FormData();
73+
files.forEach(file => {
74+
formData.append('data', file);
75+
});
76+
77+
try {
78+
const response = await fetch(EDGE_IMPULSE_INGESTION_API, {
79+
method: 'POST',
80+
headers: {
81+
'x-api-key': EDGE_IMPULSE_API_KEY,
82+
'x-label': label,
83+
},
84+
body: formData,
85+
});
86+
87+
if (response.ok) {
88+
setStatus('success');
89+
setStatusMessage('Files uploaded successfully!');
90+
setFiles([]);
91+
} else {
92+
const errorText = await response.text();
93+
setStatus('error');
94+
setStatusMessage(`Upload failed: ${errorText || response.statusText}`);
95+
}
96+
} catch (error) {
97+
setStatus('error');
98+
setStatusMessage(`An error occurred: ${error instanceof Error ? error.message : String(error)}`);
99+
}
100+
};
101+
102+
return (
103+
<div className="flex min-h-screen bg-background-dark">
104+
<Sidebar />
105+
<div className="flex-1 bg-background-darker/50">
106+
<header className="flex items-center justify-between whitespace-nowrap border-b border-solid border-surface-accent px-10 py-3">
107+
<h1 className="text-white text-lg font-bold">Data Ingestion</h1>
108+
<div className="flex items-center gap-3">
109+
<div className="bg-center bg-no-repeat aspect-square bg-cover rounded-full size-10" style={{ backgroundImage: `url("https://lh3.googleusercontent.com/aida-public/AB6AXuB-L3B8Q4Z2iFhq3U9TIcZcorNrIeIoN6x71tmWbZjmEvpXpdlYWOV2gqONYBig7NXwgw-o1cQBhNPWZ1M_Kae228Zfni7pYC4CsrpRAyiiTIf121kdnW1rqv7snNRGMwUd7l-305dNqK_7WSFtCR8NyMBUyP33BKbUwEIZ6nc_1qX58wRjlpx_-SAltx1LLGd64ncUel2q56vmFHkK0aaa02uykkz_Jun4YhFtgrjCPgQX5qxbaVjmO4oD_mJNUPFrQXSO1UUfWlbJ")` }}></div>
110+
<div>
111+
<h1 className="text-white text-base font-medium">Dr. Evelyn Reed</h1>
112+
<p className="text-text-secondary text-sm">Environmental Scientist</p>
113+
</div>
114+
</div>
115+
</header>
116+
<main className="p-10">
117+
<div className="max-w-4xl mx-auto flex flex-col gap-6">
118+
<div className="p-6 bg-surface rounded-xl flex flex-col gap-4">
119+
<h2 className="text-white text-xl font-bold">Upload to Edge Impulse</h2>
120+
<p className="text-text-secondary">Upload your sensor data files directly to your Edge Impulse project for training and analysis.</p>
121+
122+
<div className="flex flex-col gap-2">
123+
<label htmlFor="label" className="text-text-primary text-sm font-medium">Data Label</label>
124+
<input
125+
id="label"
126+
type="text"
127+
value={label}
128+
onChange={(e) => setLabel(e.target.value)}
129+
placeholder="e.g., car, background_noise, plant_healthy"
130+
className="form-input w-full rounded-lg border-surface-accent bg-background-dark text-text-primary placeholder-text-secondary focus:border-primary focus:ring-primary"
131+
/>
132+
</div>
133+
134+
<div
135+
onDragEnter={handleDragEnter}
136+
onDragLeave={handleDragLeave}
137+
onDragOver={handleDragOver}
138+
onDrop={handleDrop}
139+
onClick={openFileDialog}
140+
className={`flex flex-col items-center justify-center p-8 border-2 border-dashed rounded-lg cursor-pointer transition-colors ${isDragActive ? 'border-primary bg-primary/10' : 'border-surface-accent hover:border-primary/50'}`}
141+
>
142+
<input
143+
ref={fileInputRef}
144+
type="file"
145+
multiple
146+
onChange={handleFileSelect}
147+
className="hidden"
148+
/>
149+
<span className="material-symbols-outlined text-primary text-4xl mb-4">cloud_upload</span>
150+
<p className="text-text-primary">Drag & drop files here, or click to select files</p>
151+
<p className="text-text-secondary text-sm">Supported formats: PNG, JPG, WAV, CBOR, JSON, etc.</p>
152+
</div>
153+
</div>
154+
155+
{files.length > 0 && (
156+
<div className="p-6 bg-surface rounded-xl flex flex-col gap-4">
157+
<h3 className="text-white text-lg font-medium">Selected Files ({files.length})</h3>
158+
<ul className="space-y-2 max-h-60 overflow-y-auto pr-2">
159+
{files.map((file, index) => (
160+
<li key={`${file.name}-${index}`} className="flex items-center justify-between bg-background-dark p-3 rounded-lg">
161+
<div className="flex items-center gap-3 overflow-hidden">
162+
<span className="material-symbols-outlined text-text-secondary">description</span>
163+
<p className="text-text-primary text-sm truncate" title={file.name}>{file.name}</p>
164+
</div>
165+
<button onClick={() => removeFile(file.name)} className="text-negative hover:opacity-80 transition-opacity flex-shrink-0">
166+
<span className="material-symbols-outlined">delete</span>
167+
</button>
168+
</li>
169+
))}
170+
</ul>
171+
</div>
172+
)}
173+
174+
<div className="flex items-center gap-4 mt-2">
175+
<button
176+
onClick={handleUpload}
177+
disabled={status === 'uploading' || files.length === 0 || !label.trim()}
178+
className="flex min-w-[120px] cursor-pointer items-center justify-center rounded-lg h-12 px-5 bg-primary text-background-dark text-base font-bold transition-opacity hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed"
179+
>
180+
{status === 'uploading' ? (
181+
<>
182+
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-background-dark" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
183+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
184+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
185+
</svg>
186+
Uploading...
187+
</>
188+
) : 'Upload Files'}
189+
</button>
190+
{status !== 'idle' && status !== 'uploading' && (
191+
<p className={`text-sm ${status === 'success' ? 'text-positive' : 'text-negative'}`}>
192+
{statusMessage}
193+
</p>
194+
)}
195+
</div>
196+
</div>
197+
</main>
198+
</div>
199+
</div>
200+
);
201+
};
202+
203+
export default IngestionPage;

components/Sidebar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const Sidebar: React.FC = () => {
3535
<nav className="flex flex-col gap-2">
3636
<NavItem to="/dashboard" icon="dashboard" label="Dashboard" />
3737
<NavItem to="/analysis" icon="pie_chart" label="Analysis" isFilled={true} />
38+
<NavItem to="/ingestion" icon="upload" label="Ingestion" />
3839
<NavItem to="/reports" icon="folder" label="Reports" />
3940
<NavItem to="/settings" icon="settings" label="Settings" />
4041
</nav>

0 commit comments

Comments
 (0)