Skip to content

Commit 5adad05

Browse files
committed
feat: Initialize project structure and dependencies
Sets up the foundational files for the Airguard VisionEdge GHG Anomaly Detector. This includes: - Initializing `package.json` with core dependencies like React, React Router, and Recharts. - Configuring Vite for development and building. - Setting up TypeScript and ESLint configurations. - Defining the main application entry point (`index.tsx`) and the root `App.tsx` component. - Including basic routing structure for login, dashboard, and analysis pages. - Adding a placeholder for the copilot widget. - Configuring Tailwind CSS for styling. - Updating `README.md` with setup instructions. - Defining app metadata.
1 parent 1b99e7d commit 5adad05

16 files changed

+738
-5
lines changed

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

App.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
import React, { useState } from 'react';
3+
import { HashRouter, Routes, Route, Navigate } from 'react-router-dom';
4+
import LoginPage from './components/LoginPage';
5+
import DashboardPage from './components/DashboardPage';
6+
import AnalysisPage from './components/AnalysisPage';
7+
import CopilotWidget from './components/CopilotWidget';
8+
import { VisionEdgeIcon } from './components/icons';
9+
10+
function App() {
11+
const [isCopilotOpen, setIsCopilotOpen] = useState(false);
12+
13+
return (
14+
<HashRouter>
15+
<div className="bg-background-dark text-text-primary min-h-screen">
16+
<Routes>
17+
<Route path="/login" element={<LoginPage />} />
18+
<Route path="/dashboard" element={<DashboardPage />} />
19+
<Route path="/analysis" element={<AnalysisPage />} />
20+
<Route path="*" element={<Navigate to="/login" />} />
21+
</Routes>
22+
23+
<div className="fixed bottom-8 left-8">
24+
<button onClick={() => setIsCopilotOpen(true)} className="flex items-center gap-2 group">
25+
<VisionEdgeIcon className="text-copilot-text-secondary text-2xl group-hover:text-copilot-primary transition-colors" />
26+
<span className="text-lg font-semibold text-copilot-text-secondary group-hover:text-copilot-primary transition-colors">VisionEdge</span>
27+
</button>
28+
</div>
29+
30+
<CopilotWidget isOpen={isCopilotOpen} onClose={() => setIsCopilotOpen(false)} />
31+
</div>
32+
</HashRouter>
33+
);
34+
}
35+
36+
export default App;

README.md

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

5-
<h1>Built with AI Studio</h2>
5+
# Run and deploy your AI Studio app
66

7-
<p>The fastest path from prompt to production with Gemini.</p>
7+
This contains everything you need to run your app locally.
88

9-
<a href="https://aistudio.google.com/apps">Start building</a>
9+
View your app in AI Studio: https://ai.studio/apps/drive/1v-6gmO3NbzaoIYMnQoFR10ib-7kjQWrO
1010

11-
</div>
11+
## Run Locally
12+
13+
**Prerequisites:** Node.js
14+
15+
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/AnalysisPage.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
2+
import React, { useState } from 'react';
3+
import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, Tooltip } from 'recharts';
4+
import Sidebar from './Sidebar';
5+
6+
const chartData = [
7+
{ name: 'Week 1', value: 109 }, { name: 'Week 2', value: 21 },
8+
{ name: 'Week 3', value: 93 }, { name: 'Week 4', value: 45 },
9+
];
10+
11+
const AnalysisPage: React.FC = () => {
12+
const [activeTab, setActiveTab] = useState('AI Inference');
13+
const tabs = ['AI Inference', 'Time Series', 'Correlations', 'Ground Data'];
14+
15+
return (
16+
<div className="flex min-h-screen bg-background-dark">
17+
<Sidebar />
18+
<div className="flex-1 bg-background-darker/50">
19+
<header className="flex items-center justify-between whitespace-nowrap border-b border-solid border-surface-accent px-10 py-3">
20+
<h1 className="text-white text-lg font-bold">Analysis Panel</h1>
21+
<div className="flex items-center gap-4">
22+
<button className="flex min-w-[84px] cursor-pointer items-center justify-center rounded-lg h-10 px-4 bg-primary text-background-dark text-sm font-bold hover:bg-primary/80 transition-colors">
23+
Open in Colab
24+
</button>
25+
<div className="flex items-center gap-3">
26+
<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>
27+
<div>
28+
<h1 className="text-white text-base font-medium">Dr. Evelyn Reed</h1>
29+
<p className="text-text-secondary text-sm">Environmental Scientist</p>
30+
</div>
31+
</div>
32+
</div>
33+
</header>
34+
<main className="p-10">
35+
<div className="flex flex-col gap-6">
36+
<div className="border-b border-surface-accent">
37+
<div className="flex gap-8">
38+
{tabs.map(tab => (
39+
<button key={tab} onClick={() => setActiveTab(tab)} className={`pb-3 pt-4 text-sm font-bold tracking-[0.015em] transition-colors border-b-[3px] ${activeTab === tab ? 'border-primary text-white' : 'border-transparent text-text-secondary hover:border-primary/50 hover:text-white'}`}>
40+
{tab}
41+
</button>
42+
))}
43+
</div>
44+
</div>
45+
<div className="flex gap-3 flex-wrap">
46+
<button className="flex h-8 items-center justify-center gap-x-2 rounded-lg bg-surface-accent px-4 hover:bg-surface-accent/70 transition-colors">
47+
<p className="text-white text-sm font-medium">Date Range</p>
48+
<span className="material-symbols-outlined text-white text-base">expand_more</span>
49+
</button>
50+
<button className="flex h-8 items-center justify-center gap-x-2 rounded-lg bg-surface-accent px-4 hover:bg-surface-accent/70 transition-colors">
51+
<p className="text-white text-sm font-medium">Location</p>
52+
<span className="material-symbols-outlined text-white text-base">expand_more</span>
53+
</button>
54+
<button className="flex h-8 items-center justify-center gap-x-2 rounded-lg bg-surface-accent px-4 hover:bg-surface-accent/70 transition-colors">
55+
<p className="text-white text-sm font-medium">Gas Type</p>
56+
<span className="material-symbols-outlined text-white text-base">expand_more</span>
57+
</button>
58+
</div>
59+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
60+
<div className="lg:col-span-2 flex flex-col gap-4 p-6 bg-surface rounded-xl">
61+
<p className="text-white text-base font-medium">AI Inference Analysis - Anomaly Hotspots</p>
62+
<img alt="Map showing anomaly hotspots" className="w-full h-[400px] object-cover rounded-lg" src="https://lh3.googleusercontent.com/aida-public/AB6AXuA0uYqAhgUj1VlP2T0JvUQZqsRBPI17CSUJQtI4sc5PngAFXGr4CC9biIoEmKoe7aljwNPqWSZFRFdcTNpBcCjdc67IBOBwr5pVJyhSBuDTsunj_D10UKQCmrSaS0ElpJYZ0sE0yF9x0EzUe6Rhfv06C2vebMwDJ-HyPdFIjwKy9kOWjM6mAdY9MyrESGagkE-SPDNt1mGNHQXY1fiabm_HmltP9jeLBEY_gCkfeMkkeR9xDOqQqMPbm5pLn8DerEY7cy1t9a8Fsf7a" />
63+
</div>
64+
<div className="flex flex-col gap-4 p-6 bg-surface rounded-xl">
65+
<div className="flex flex-col gap-2">
66+
<p className="text-white text-base font-medium">CO2 Concentration</p>
67+
<p className="text-primary text-3xl font-bold">450 ppm</p>
68+
<div className="flex gap-1">
69+
<p className="text-text-secondary text-base">Last 30 Days</p>
70+
<p className="text-positive text-base font-medium">+5%</p>
71+
</div>
72+
</div>
73+
<div className="h-48 mt-4">
74+
<ResponsiveContainer width="100%" height="100%">
75+
<AreaChart data={chartData} margin={{ top: 5, right: 20, left: -20, bottom: 5 }}>
76+
<defs>
77+
<linearGradient id="chartGradient" x1="0" y1="0" x2="0" y2="1">
78+
<stop offset="5%" stopColor="#13ecb6" stopOpacity={0.4}/>
79+
<stop offset="95%" stopColor="#13ecb6" stopOpacity={0}/>
80+
</linearGradient>
81+
</defs>
82+
<XAxis dataKey="name" stroke="#9db9b2" fontSize={12} tickLine={false} axisLine={false} />
83+
<Tooltip contentStyle={{ backgroundColor: '#111816', border: '1px solid #13ecb6' }} />
84+
<Area type="monotone" dataKey="value" stroke="#13ecb6" strokeWidth={3} fill="url(#chartGradient)" />
85+
</AreaChart>
86+
</ResponsiveContainer>
87+
</div>
88+
</div>
89+
</div>
90+
</div>
91+
</main>
92+
</div>
93+
</div>
94+
);
95+
};
96+
97+
export default AnalysisPage;

components/CopilotWidget.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
2+
import React, { useState, useRef, useEffect } from 'react';
3+
import { VisionEdgeIcon } from './icons';
4+
import { Message } from '../types';
5+
6+
interface CopilotWidgetProps {
7+
isOpen: boolean;
8+
onClose: () => void;
9+
}
10+
11+
const CopilotWidget: React.FC<CopilotWidgetProps> = ({ isOpen, onClose }) => {
12+
const [messages, setMessages] = useState<Message[]>([
13+
{ sender: 'copilot', text: "Welcome! How can I help you analyze environmental data today? Try asking: 'Explain today's emission spike in Cairo region.'" },
14+
{ sender: 'user', text: "Explain today’s emission spike in Cairo region." },
15+
{ sender: 'copilot', text: "The emission spike in the Cairo region today is primarily attributed to a significant increase in industrial activity, combined with specific meteorological conditions that trapped pollutants. Further analysis suggests a 40% increase in NOx emissions from the industrial sector compared to the daily average." }
16+
]);
17+
const [inputValue, setInputValue] = useState('');
18+
const chatContainerRef = useRef<HTMLDivElement>(null);
19+
20+
useEffect(() => {
21+
if (isOpen && chatContainerRef.current) {
22+
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
23+
}
24+
}, [messages, isOpen]);
25+
26+
if (!isOpen) {
27+
return null;
28+
}
29+
30+
const handleSendMessage = (e: React.FormEvent) => {
31+
e.preventDefault();
32+
if (inputValue.trim()) {
33+
setMessages([...messages, { sender: 'user', text: inputValue.trim() }]);
34+
setInputValue('');
35+
// Here you would typically call an API and get a response
36+
}
37+
};
38+
39+
40+
return (
41+
<div className="fixed inset-0 bg-black/50 z-40 flex items-end justify-end" onClick={onClose}>
42+
<div className="fixed bottom-8 right-8 w-full max-w-md" onClick={e => e.stopPropagation()}>
43+
<div className="flex flex-col rounded-xl bg-copilot-widget shadow-2xl animate-fade-in-up">
44+
<header className="flex items-center justify-between border-b border-white/10 px-6 py-4">
45+
<div className="flex items-center gap-3">
46+
<VisionEdgeIcon className="text-copilot-primary text-2xl" />
47+
<h2 className="text-lg font-medium text-copilot-text-primary">Ask VisionEdge Copilot</h2>
48+
</div>
49+
<button onClick={onClose} className="text-copilot-text-secondary hover:text-copilot-text-primary">
50+
<span className="material-symbols-outlined">close</span>
51+
</button>
52+
</header>
53+
<div ref={chatContainerRef} className="flex-1 space-y-6 overflow-y-auto p-6" style={{ maxHeight: '50vh' }}>
54+
{messages.map((msg, index) => (
55+
<div key={index} className={`flex items-end gap-3 ${msg.sender === 'user' ? 'justify-end' : ''}`}>
56+
{msg.sender === 'copilot' && (
57+
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-copilot-primary/20">
58+
<span className="material-symbols-outlined text-copilot-primary text-xl">auto_awesome</span>
59+
</div>
60+
)}
61+
<div className={`flex flex-col gap-1 ${msg.sender === 'user' ? 'items-end' : ''}`}>
62+
<p className="text-sm text-copilot-text-secondary">{msg.sender === 'user' ? 'You' : 'VisionEdge Copilot'}</p>
63+
<div className={`rounded-lg px-4 py-3 ${msg.sender === 'user' ? 'rounded-br-none bg-copilot-primary text-copilot-background' : 'rounded-bl-none bg-zinc-800 text-copilot-text-primary'}`}>
64+
<p className="text-base font-normal leading-normal">{msg.text}</p>
65+
</div>
66+
</div>
67+
</div>
68+
))}
69+
</div>
70+
<div className="border-t border-white/10 px-6 py-4">
71+
<form onSubmit={handleSendMessage} className="relative">
72+
<input
73+
className="form-input w-full rounded-lg border-none bg-zinc-800 py-3 pl-4 pr-12 text-base text-copilot-text-primary placeholder-copilot-text-secondary focus:outline-none focus:ring-2 focus:ring-copilot-primary/50"
74+
placeholder="Ask about environmental trends..."
75+
type="text"
76+
value={inputValue}
77+
onChange={(e) => setInputValue(e.target.value)}
78+
/>
79+
<button type="submit" className="absolute inset-y-0 right-0 flex items-center justify-center px-4 text-copilot-text-secondary hover:text-copilot-primary">
80+
<span className="material-symbols-outlined">send</span>
81+
</button>
82+
</form>
83+
</div>
84+
</div>
85+
</div>
86+
<style>{`
87+
@keyframes fade-in-up {
88+
from { opacity: 0; transform: translateY(20px); }
89+
to { opacity: 1; transform: translateY(0); }
90+
}
91+
.animate-fade-in-up {
92+
animation: fade-in-up 0.3s ease-out forwards;
93+
}
94+
`}</style>
95+
</div>
96+
);
97+
};
98+
99+
export default CopilotWidget;

0 commit comments

Comments
 (0)