Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7148e1026e | ||
|
|
96f337ef46 | ||
|
|
3b5839ebd5 | ||
|
|
e2742a7a60 | ||
|
|
ae22adee40 | ||
|
|
ee02bd4f9a | ||
|
|
310d8adc8d | ||
|
|
ccbcd99053 | ||
|
|
52e60b99dd | ||
|
|
e19321a2b7 |
@@ -4,7 +4,7 @@
|
|||||||
<h1 align="center">ZeaVis Edu</h1>
|
<h1 align="center">ZeaVis Edu</h1>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>Asisten Edukasi Interaktif untuk Deteksi Penyakit Daun Jagung</strong><br>
|
<strong>Asisten Edukasi Interaktif untuk Deteksi Penyakit Daun Jagung</strong><br>
|
||||||
<em>Computer Vision • EfficientNetV2B0 • Transfer Learning</em>
|
<em>Computer Vision • EfficientNetV2B0 • Rust ONNX Runtime • Tauri 2 Android</em>
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
<a href="#-jadwal"><b>Jadwal</b></a> •
|
<a href="#-jadwal"><b>Jadwal</b></a> •
|
||||||
<a href="#-tech-stack"><b>Tech Stack</b></a> •
|
<a href="#-tech-stack"><b>Tech Stack</b></a> •
|
||||||
<a href="#-memulai"><b>Memulai</b></a> •
|
<a href="#-memulai"><b>Memulai</b></a> •
|
||||||
|
<a href="#-platform"><b>Platform</b></a> •
|
||||||
<a href="#-dokumentasi"><b>Dokumentasi</b></a>
|
<a href="#-dokumentasi"><b>Dokumentasi</b></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
|
|||||||
| Klasifikasi 3 penyakit + 1 daun sehat | Penyakit pada batang atau buah jagung |
|
| Klasifikasi 3 penyakit + 1 daun sehat | Penyakit pada batang atau buah jagung |
|
||||||
| Deteksi berbasis unggah gambar daun | Prediksi tanpa input gambar |
|
| Deteksi berbasis unggah gambar daun | Prediksi tanpa input gambar |
|
||||||
| Rekomendasi obat & penanganan | Diagnosis pengganti ahli/POPT |
|
| Rekomendasi obat & penanganan | Diagnosis pengganti ahli/POPT |
|
||||||
| Aplikasi web edukatif | Aplikasi mobile native |
|
| Aplikasi Web + Android (Tauri 2) | Aplikasi iOS |
|
||||||
|
|
||||||
### 4 Kelas yang Diklasifikasikan
|
### 4 Kelas yang Diklasifikasikan
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
|
|||||||
| 2 | **Model ML** | Model Computer Vision terlatih di Google Colab, siap produksi |
|
| 2 | **Model ML** | Model Computer Vision terlatih di Google Colab, siap produksi |
|
||||||
| 3 | **UI Antarmuka** | Front-End berbasis React + Vite dengan fitur unggah gambar |
|
| 3 | **UI Antarmuka** | Front-End berbasis React + Vite dengan fitur unggah gambar |
|
||||||
| 4 | **Back-End Integration** | API + ML Service untuk inferensi real-time via Docker |
|
| 4 | **Back-End Integration** | API + ML Service untuk inferensi real-time via Docker |
|
||||||
| 5 | **Prototipe Akhir** | Aplikasi web final dengan klasifikasi + modul edukasi (rekomendasi obat & penanganan) |
|
| 5 | **Prototipe Akhir** | Aplikasi Web + Android (Tauri 2) dengan klasifikasi & modul edukasi (rekomendasi obat & penanganan) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -131,7 +132,8 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
|
|||||||
.
|
.
|
||||||
├── apps/
|
├── apps/
|
||||||
│ ├── api/ # Backend Elysia/Bun + Drizzle ORM + PostgreSQL
|
│ ├── api/ # Backend Elysia/Bun + Drizzle ORM + PostgreSQL
|
||||||
│ ├── ml-service/ # Rust/Axum + ONNX Runtime inference service
|
│ ├── ml-service/ # Rust/Axum + ONNX Runtime inference engine
|
||||||
|
│ ├── tauri/ # Tauri 2 mobile wrapper → Android APK
|
||||||
│ └── web/ # Frontend React + Vite + Tailwind CSS
|
│ └── web/ # Frontend React + Vite + Tailwind CSS
|
||||||
├── Machine_Learning/ # Pipeline dataset, training Colab, ekspor model
|
├── Machine_Learning/ # Pipeline dataset, training Colab, ekspor model
|
||||||
│ └── README.md # ⤷ Panduan lengkap pipeline ML
|
│ └── README.md # ⤷ Panduan lengkap pipeline ML
|
||||||
@@ -147,8 +149,9 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
|
|||||||
| Komponen | Teknologi | Dokumentasi |
|
| Komponen | Teknologi | Dokumentasi |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Web Frontend | React, Vite, Tailwind, Zustand, TanStack Query | `apps/web/` |
|
| Web Frontend | React, Vite, Tailwind, Zustand, TanStack Query | `apps/web/` |
|
||||||
|
| Android App | Tauri 2, Rust, WebView, Deep Link OAuth | `apps/tauri/` |
|
||||||
| API Backend | Bun, Elysia, Drizzle ORM, PostgreSQL | `apps/api/` |
|
| API Backend | Bun, Elysia, Drizzle ORM, PostgreSQL | `apps/api/` |
|
||||||
| ML Inference | Rust, Axum, ONNX Runtime | [`apps/ml-service/README.md`](apps/ml-service/README.md) |
|
| ML Inference Engine | Rust, Axum, ONNX Runtime | [`apps/ml-service/README.md`](apps/ml-service/README.md) |
|
||||||
| ML Pipeline | Python, TensorFlow/Keras, EfficientNetV2B0 | [`Machine_Learning/README.md`](Machine_Learning/README.md) |
|
| ML Pipeline | Python, TensorFlow/Keras, EfficientNetV2B0 | [`Machine_Learning/README.md`](Machine_Learning/README.md) |
|
||||||
| Infrastruktur | Docker, Coolify, Traefik, Tailscale | [`infra/README.md`](infra/README.md) |
|
| Infrastruktur | Docker, Coolify, Traefik, Tailscale | [`infra/README.md`](infra/README.md) |
|
||||||
| Telemetry | Prometheus, ClickHouse, Vector, Vue 3 | `telemetry/` |
|
| Telemetry | Prometheus, ClickHouse, Vector, Vue 3 | `telemetry/` |
|
||||||
@@ -157,15 +160,18 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
|
|||||||
|
|
||||||
## 🛠️ Tech Stack
|
## 🛠️ Tech Stack
|
||||||
|
|
||||||
### Frontend
|
### Frontend & Mobile
|
||||||
React • Vite • TypeScript • React Router • TanStack Query • Zustand • Tailwind CSS
|
React • Vite • TypeScript • React Router • TanStack Query • Zustand • Tailwind CSS
|
||||||
|
**Tauri 2** (Android) • Rust • WebView • Deep Link OAuth
|
||||||
|
|
||||||
### Backend API
|
### Backend API
|
||||||
Bun • Elysia • Drizzle ORM • PostgreSQL • prom-client
|
Bun • Elysia • Drizzle ORM • PostgreSQL • prom-client
|
||||||
|
|
||||||
### Machine Learning
|
### Machine Learning
|
||||||
Python • TensorFlow/Keras • EfficientNetV2B0 • Google Colab (GPU T4)
|
Python • TensorFlow/Keras • EfficientNetV2B0 • Google Colab (GPU T4)
|
||||||
Rust • Axum • ONNX Runtime • TFLite • TensorFlow.js
|
|
||||||
|
### Inference Engine
|
||||||
|
**Rust** • **Axum** • **ONNX Runtime** • TFLite • TensorFlow.js
|
||||||
|
|
||||||
### DevOps & Infrastruktur
|
### DevOps & Infrastruktur
|
||||||
Docker • Docker Compose • Coolify • Traefik • Tailscale • GitHub Actions (CI/CD)
|
Docker • Docker Compose • Coolify • Traefik • Tailscale • GitHub Actions (CI/CD)
|
||||||
@@ -181,7 +187,8 @@ Prometheus • Metric Ingester (Go) • Vector • ClickHouse • Qu
|
|||||||
|
|
||||||
- **Bun** — runtime & package manager
|
- **Bun** — runtime & package manager
|
||||||
- **Python 3.9–3.11** — pipeline ML
|
- **Python 3.9–3.11** — pipeline ML
|
||||||
- **Rust & Cargo** — `apps/ml-service`
|
- **Rust & Cargo** — `apps/ml-service` (inference) & `apps/tauri` (Android)
|
||||||
|
- **Java 21 + Android SDK** — build Android APK
|
||||||
- **Docker & Docker Compose** — deployment & telemetry
|
- **Docker & Docker Compose** — deployment & telemetry
|
||||||
- **PostgreSQL** — backend API
|
- **PostgreSQL** — backend API
|
||||||
|
|
||||||
@@ -199,7 +206,9 @@ bun install
|
|||||||
bun run dev # Semua service (web + api)
|
bun run dev # Semua service (web + api)
|
||||||
cd apps/web && bun run dev # Hanya frontend
|
cd apps/web && bun run dev # Hanya frontend
|
||||||
cd apps/api && bun run start # Hanya backend API
|
cd apps/api && bun run start # Hanya backend API
|
||||||
cd apps/ml-service && cargo run # Hanya ML service (port 8000)
|
cd apps/ml-service && cargo run # ML inference engine (port 8000)
|
||||||
|
cd apps/tauri && bun run tauri dev # Tauri desktop dev
|
||||||
|
cd apps/tauri && bun run tauri android dev # Tauri Android dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
@@ -236,6 +245,46 @@ make telemetry-up # Telemetry stack
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 📱 Platform
|
||||||
|
|
||||||
|
ZeaVis Edu tersedia di **dua platform** dari satu codebase:
|
||||||
|
|
||||||
|
| Platform | Teknologi | Build |
|
||||||
|
|---|---|---|
|
||||||
|
| **Web** | React + Vite → Static SPA | `bun run build` |
|
||||||
|
| **Android** | Tauri 2 + Rust → WebView APK | `cd apps/tauri && bun run tauri android build --apk` |
|
||||||
|
|
||||||
|
### Tauri 2 Android
|
||||||
|
|
||||||
|
Aplikasi Android membungkus frontend web yang sama dalam **WebView native** menggunakan **Tauri 2**, memberikan akses ke API native Android tanpa menulis ulang UI.
|
||||||
|
|
||||||
|
**Fitur Android:**
|
||||||
|
- **Google OAuth** — Login via system browser + deep link `zeavisedu://` kembali ke app
|
||||||
|
- **Kamera** — Izin `CAMERA` untuk unggah foto daun jagung langsung dari kamera
|
||||||
|
- **Tauri Plugin Opener** — Buka URL eksternal di system browser
|
||||||
|
- **Tauri Plugin Deep Link** — Tangkap OAuth callback tanpa memerlukan server redirect
|
||||||
|
|
||||||
|
**CI/CD Android:**
|
||||||
|
- GitHub Actions workflow `.github/workflows/android.yml`
|
||||||
|
- Build otomatis di setiap push/PR ke `main`
|
||||||
|
- Patch `AndroidManifest.xml` untuk menambahkan izin kamera + intent filter deep link
|
||||||
|
- APK ditandatangani (signed) via `apksigner` + release ke GitHub Releases
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development Android (butuh Android SDK + emulator/device)
|
||||||
|
cd apps/tauri
|
||||||
|
bun run tauri android init # Init project Android
|
||||||
|
bun run tauri android dev # Dev dengan hot reload
|
||||||
|
bun run tauri android build --apk # Build APK production
|
||||||
|
|
||||||
|
# CI/CD — dijalankan otomatis via GitHub Actions
|
||||||
|
.github/workflows/android.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
> Konfigurasi: `apps/tauri/tauri.conf.json` • `apps/tauri/gen/android/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📚 Dokumentasi
|
## 📚 Dokumentasi
|
||||||
|
|
||||||
| Dokumen | Isi |
|
| Dokumen | Isi |
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ function renderTauriDeepLinkPage(targetUrl: string): Response {
|
|||||||
const escapedPath = pathAndQuery.replace(/"/g, '"');
|
const escapedPath = pathAndQuery.replace(/"/g, '"');
|
||||||
// intent:// scheme: Chrome on Android opens the target app by package name
|
// intent:// scheme: Chrome on Android opens the target app by package name
|
||||||
// browser_fallback_url: shown if the app isn't installed
|
// browser_fallback_url: shown if the app isn't installed
|
||||||
const intentUrl = `intent:${escapedPath}#Intent;scheme=zeavisedu;package=com.zeavis.edu;S.browser_fallback_url=${encodeURIComponent(targetUrl)};end`;
|
// Use intent://login/... to produce data URI zeavisedu://login/login?token=xxx
|
||||||
|
// which new URL() can parse (single-slash non-hierarchical URLs break WebView)
|
||||||
|
const intentUrl = `intent://login${escapedPath}#Intent;scheme=zeavisedu;package=com.zeavis.edu;S.browser_fallback_url=${encodeURIComponent(targetUrl)};end`;
|
||||||
|
|
||||||
const html = `<!DOCTYPE html>
|
const html = `<!DOCTYPE html>
|
||||||
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
@@ -312,12 +314,18 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
|||||||
}
|
}
|
||||||
|
|
||||||
const token = await createSession(user.id);
|
const token = await createSession(user.id);
|
||||||
set.headers['Set-Cookie'] = createSessionCookie(token, request.headers);
|
const sessionCookie = createSessionCookie(token, request.headers);
|
||||||
|
|
||||||
authCounter.labels('login', 'true').inc();
|
authCounter.labels('login', 'true').inc();
|
||||||
|
|
||||||
const successUrl = `${env.webAppUrl}/login?token=${encodeURIComponent(token)}`;
|
const successUrl = `${env.webAppUrl}/login?token=${encodeURIComponent(token)}`;
|
||||||
if (platform === 'tauri') return renderTauriDeepLinkPage(successUrl);
|
if (platform === 'tauri') {
|
||||||
|
// Inject Set-Cookie into the response so the browser gets it on redirect
|
||||||
|
const resp = renderTauriDeepLinkPage(successUrl);
|
||||||
|
resp.headers.set('Set-Cookie', sessionCookie);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
set.headers['Set-Cookie'] = sessionCookie;
|
||||||
set.status = 302;
|
set.status = 302;
|
||||||
set.headers['Location'] = successUrl;
|
set.headers['Location'] = successUrl;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.zeavis.edu",
|
"identifier": "com.zeavis.edu",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../web/dist",
|
"frontendDist": "../web/dist-tauri",
|
||||||
"devUrl": "http://localhost:5173",
|
"devUrl": "http://localhost:5173",
|
||||||
"beforeBuildCommand": "cd ../web && bun run build",
|
"beforeBuildCommand": "echo 'Nothing to build (APK loads live web content)'",
|
||||||
"beforeDevCommand": "cd ../web && bun run dev"
|
"beforeDevCommand": "echo 'Dev mode - connect to localhost:5173'"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"withGlobalTauri": false,
|
"withGlobalTauri": false,
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>ZeaVis Edu</title>
|
||||||
|
<script>
|
||||||
|
var LIVE = 'https://zeavisedu.asepharyana.my.id';
|
||||||
|
var T = window.__TAURI_INTERNALS__;
|
||||||
|
|
||||||
|
function navigate(path) {
|
||||||
|
window.location.replace(LIVE + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// On cold start, check if the app was opened via a deep link (Google OAuth)
|
||||||
|
// before redirecting to the live web app.
|
||||||
|
if (T && T.invoke) {
|
||||||
|
T.invoke('plugin:deep-link|get_current')
|
||||||
|
.then(function(urls) {
|
||||||
|
if (urls && urls.length > 0 && urls[0]) {
|
||||||
|
try {
|
||||||
|
var u = new URL(urls[0]);
|
||||||
|
var target = u.pathname + u.search + u.hash;
|
||||||
|
if (target && target !== '/') {
|
||||||
|
// Preserve full path + query (e.g. /login?token=xxx)
|
||||||
|
navigate(target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) { /* malformed URL — fall through */ }
|
||||||
|
}
|
||||||
|
// No deep link — redirect to live app home
|
||||||
|
navigate('/');
|
||||||
|
})
|
||||||
|
.catch(function() { navigate('/'); });
|
||||||
|
} else {
|
||||||
|
// Not in Tauri (dev mode or unknown) — redirect to live
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body style="background:#f0fdf4;font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0">
|
||||||
|
<p style="color:#16a34a">Memuat ZeaVis Edu...</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tanstack/react-query": "^5.100.11",
|
"@tanstack/react-query": "^5.100.11",
|
||||||
|
"@tauri-apps/plugin-deep-link": "^2",
|
||||||
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"@zeavis/shared": "workspace:*",
|
"@zeavis/shared": "workspace:*",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 103 KiB |
+31
-16
@@ -29,27 +29,42 @@ export async function openUrl(url: string): Promise<void> {
|
|||||||
* callback page redirects to zeavisedu://... which triggers this listener.
|
* callback page redirects to zeavisedu://... which triggers this listener.
|
||||||
* We extract the path + query and navigate the WebView there.
|
* We extract the path + query and navigate the WebView there.
|
||||||
*/
|
*/
|
||||||
|
function processDeepLinkUrl(url: string): void {
|
||||||
|
// url looks like: zeavisedu://login/login?token=xxx
|
||||||
|
// We need to extract path+query and navigate the WebView there
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
const target = u.pathname + u.search + u.hash;
|
||||||
|
if (target && target !== '/') {
|
||||||
|
window.location.href = target;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch { /* try fallback below */ }
|
||||||
|
|
||||||
|
// Fallback: handle both double-slash (://) and single-slash (:/) schemes
|
||||||
|
let match = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
|
||||||
|
if (!match) {
|
||||||
|
match = url.match(/^[^:]+:\/(\/.*)?$/);
|
||||||
|
}
|
||||||
|
if (match?.[1]) {
|
||||||
|
window.location.href = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function setupDeepLinkHandler(): Promise<void> {
|
export async function setupDeepLinkHandler(): Promise<void> {
|
||||||
if (!isTauri()) return;
|
if (!isTauri()) return;
|
||||||
|
|
||||||
const { onOpenUrl } = await import('@tauri-apps/plugin-deep-link');
|
const { onOpenUrl, getCurrent } = await import('@tauri-apps/plugin-deep-link');
|
||||||
|
|
||||||
|
// Handle cold-start deep links (app opened via intent)
|
||||||
|
getCurrent().then((urls) => {
|
||||||
|
if (urls?.[0]) processDeepLinkUrl(urls[0]);
|
||||||
|
}).catch(() => { /* ignore */ });
|
||||||
|
|
||||||
|
// Handle warm-start deep links (app already running, new intent received)
|
||||||
onOpenUrl((urls) => {
|
onOpenUrl((urls) => {
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
// url looks like: zeavisedu://zeavisedu.asepharyana.my.id/login?token=xxx
|
processDeepLinkUrl(url);
|
||||||
// Extract path + query after the host
|
|
||||||
try {
|
|
||||||
const u = new URL(url);
|
|
||||||
const target = u.pathname + u.search + u.hash;
|
|
||||||
if (target && target !== '/') {
|
|
||||||
window.location.href = target;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// If URL parsing fails, try to extract everything after the scheme
|
|
||||||
const match = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
|
|
||||||
if (match?.[1]) {
|
|
||||||
window.location.href = match[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,12 +54,12 @@ export function CatalogPage() {
|
|||||||
<div className="space-y-6 max-w-5xl mx-auto pb-10">
|
<div className="space-y-6 max-w-5xl mx-auto pb-10">
|
||||||
{/* Page Title */}
|
{/* Page Title */}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl md:text-3xl font-extrabold text-[#214B11]">
|
<h1 className="text-2xl font-bold text-emerald-800">
|
||||||
Pustaka Penyakit
|
Pustaka Penyakit
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-muted-foreground text-sm md:text-base">
|
<p className="text-gray-500 mt-1 text-md">
|
||||||
Referensi lengkap penyakit dan kondisi daun jagung yang dapat
|
Referensi lengkap penyakit dan kondisi daun jagung yang dapat
|
||||||
dideteksi oleh sistem AI ZeaVis Edu.
|
dideteksi oleh sistem AI ZeaVis Edu
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { useUiStore } from "@/store/ui-store";
|
import { useUiStore } from "@/store/ui-store";
|
||||||
import { apiClient } from "@/lib/api-client";
|
import { apiClient } from "@/lib/api-client";
|
||||||
import bg from "@/assets/images/dashboard-bg.webp";
|
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const { dashboardCompact } = useUiStore();
|
const { dashboardCompact } = useUiStore();
|
||||||
@@ -23,16 +22,17 @@ export function DashboardPage() {
|
|||||||
const summary = summaryQuery.data;
|
const summary = summaryQuery.data;
|
||||||
const diseases = diseasesQuery.data ?? [];
|
const diseases = diseasesQuery.data ?? [];
|
||||||
|
|
||||||
const diseasesQuick = useMemo(() =>
|
const diseasesQuick = useMemo(
|
||||||
diseases
|
() =>
|
||||||
.sort((a, b) => a.displayOrder - b.displayOrder)
|
diseases
|
||||||
.map((d) => ({
|
.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||||
name: d.commonName,
|
.map((d) => ({
|
||||||
sci: d.label,
|
name: d.commonName,
|
||||||
color: d.accentColor,
|
sci: d.label,
|
||||||
slug: d.slug,
|
color: d.accentColor,
|
||||||
})),
|
slug: d.slug,
|
||||||
[diseases]
|
})),
|
||||||
|
[diseases],
|
||||||
);
|
);
|
||||||
|
|
||||||
const missionCards = [
|
const missionCards = [
|
||||||
@@ -74,7 +74,11 @@ export function DashboardPage() {
|
|||||||
{/* Hero header */}
|
{/* Hero header */}
|
||||||
<header
|
<header
|
||||||
className="relative overflow-hidden rounded-3xl bg-cover bg-center bg-no-repeat shadow-sm"
|
className="relative overflow-hidden rounded-3xl bg-cover bg-center bg-no-repeat shadow-sm"
|
||||||
style={{ backgroundImage: `url(${bg})` }}
|
style={{
|
||||||
|
backgroundImage: `url(https://cdn.pixabay.com/photo/2014/09/09/19/07/corn-field-440338_1280.jpg)`,
|
||||||
|
backgroundPosition: "bottom",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-[#2F6E1A]/60 to-black/30" />
|
<div className="absolute inset-0 bg-gradient-to-b from-[#2F6E1A]/60 to-black/30" />
|
||||||
<div className="relative z-10 flex flex-col md:flex-row items-center justify-between gap-6 p-6 md:p-10">
|
<div className="relative z-10 flex flex-col md:flex-row items-center justify-between gap-6 p-6 md:p-10">
|
||||||
@@ -82,7 +86,9 @@ export function DashboardPage() {
|
|||||||
<span className="inline-block rounded-full bg-[#1E8A2A]/80 px-4 py-2 text-xs font-semibold">
|
<span className="inline-block rounded-full bg-[#1E8A2A]/80 px-4 py-2 text-xs font-semibold">
|
||||||
AI FOR SMART EDUCATION
|
AI FOR SMART EDUCATION
|
||||||
</span>
|
</span>
|
||||||
<h1 className="text-2xl md:text-4xl font-extrabold">Selamat Datang di</h1>
|
<h1 className="text-2xl md:text-4xl font-extrabold">
|
||||||
|
Selamat Datang di
|
||||||
|
</h1>
|
||||||
<h2 className="text-2xl md:text-4xl font-extrabold tracking-tight text-[#9AD872]">
|
<h2 className="text-2xl md:text-4xl font-extrabold tracking-tight text-[#9AD872]">
|
||||||
ZeaVis Edu
|
ZeaVis Edu
|
||||||
</h2>
|
</h2>
|
||||||
@@ -107,10 +113,7 @@ export function DashboardPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="px-4 md:px-6 py-3 md:py-6 text-sm md:text-lg font-semibold text-white hover:bg-[#1E8A2A]"
|
className="px-4 md:px-6 py-3 md:py-6 text-sm md:text-lg font-semibold text-white hover:bg-[#1E8A2A]"
|
||||||
>
|
>
|
||||||
<Link
|
<Link to="/catalog" className="inline-flex items-center gap-2">
|
||||||
to="/catalog"
|
|
||||||
className="inline-flex items-center gap-2"
|
|
||||||
>
|
|
||||||
Pustaka Penyakit
|
Pustaka Penyakit
|
||||||
<ChevronRight className="h-5 w-6" />
|
<ChevronRight className="h-5 w-6" />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -169,7 +172,10 @@ export function DashboardPage() {
|
|||||||
<div className="text-3xl font-bold">
|
<div className="text-3xl font-bold">
|
||||||
{summary.imageClassificationCount}
|
{summary.imageClassificationCount}
|
||||||
</div>
|
</div>
|
||||||
<Link to="/diagnoses" className="text-emerald-600 ml-auto hover:underline">
|
<Link
|
||||||
|
to="/diagnoses"
|
||||||
|
className="text-emerald-600 ml-auto hover:underline"
|
||||||
|
>
|
||||||
Lihat daftar
|
Lihat daftar
|
||||||
</Link>
|
</Link>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -186,7 +192,10 @@ export function DashboardPage() {
|
|||||||
<div className="text-3xl font-bold text-amber-600">
|
<div className="text-3xl font-bold text-amber-600">
|
||||||
{summary.needsReviewCount}
|
{summary.needsReviewCount}
|
||||||
</div>
|
</div>
|
||||||
<Link to="/diagnoses?status=needs_review" className="text-amber-600 ml-auto hover:underline">
|
<Link
|
||||||
|
to="/diagnoses?status=needs_review"
|
||||||
|
className="text-amber-600 ml-auto hover:underline"
|
||||||
|
>
|
||||||
Lihat daftar
|
Lihat daftar
|
||||||
</Link>
|
</Link>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -200,10 +209,11 @@ export function DashboardPage() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="h-full flex flex-col justify-start pt-2">
|
<CardContent className="h-full flex flex-col justify-start pt-2">
|
||||||
<div className="text-3xl font-bold text-red-600">
|
<div className="text-3xl font-bold text-red-600">—</div>
|
||||||
—
|
<Link
|
||||||
</div>
|
to="/diagnoses?status=failed"
|
||||||
<Link to="/diagnoses?status=failed" className="text-red-600 ml-auto hover:underline">
|
className="text-red-600 ml-auto hover:underline"
|
||||||
|
>
|
||||||
Lihat daftar
|
Lihat daftar
|
||||||
</Link>
|
</Link>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -220,7 +230,10 @@ export function DashboardPage() {
|
|||||||
<div className="text-3xl font-bold text-red-600">
|
<div className="text-3xl font-bold text-red-600">
|
||||||
{summary.riskDistribution.high}
|
{summary.riskDistribution.high}
|
||||||
</div>
|
</div>
|
||||||
<Link to="/catalog?risk=high" className="text-red-600 ml-auto hover:underline">
|
<Link
|
||||||
|
to="/catalog?risk=high"
|
||||||
|
className="text-red-600 ml-auto hover:underline"
|
||||||
|
>
|
||||||
Lihat pustaka
|
Lihat pustaka
|
||||||
</Link>
|
</Link>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -276,7 +289,8 @@ export function DashboardPage() {
|
|||||||
Penyakit yang Dapat Dideteksi
|
Penyakit yang Dapat Dideteksi
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[15px] font-normal text-muted-foreground">
|
<p className="text-[15px] font-normal text-muted-foreground">
|
||||||
{diseases.length} kelas penyakit dan kondisi daun jagung dalam sistem kami
|
{diseases.length} kelas penyakit dan kondisi daun jagung dalam
|
||||||
|
sistem kami
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
@@ -295,9 +309,7 @@ export function DashboardPage() {
|
|||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{diseasesQuick.map((d) => (
|
{diseasesQuick.map((d) => (
|
||||||
<Link key={d.slug} to={`/catalog/${d.slug}`}>
|
<Link key={d.slug} to={`/catalog/${d.slug}`}>
|
||||||
<Card
|
<Card className="rounded-2xl bg-white p-4 shadow-sm h-full transition-transform hover:scale-[1.03] hover:shadow-md cursor-pointer">
|
||||||
className="rounded-2xl bg-white p-4 shadow-sm h-full transition-transform hover:scale-[1.03] hover:shadow-md cursor-pointer"
|
|
||||||
>
|
|
||||||
<CardContent className="h-full p-4 flex flex-col justify-between">
|
<CardContent className="h-full p-4 flex flex-col justify-between">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div
|
<div
|
||||||
@@ -324,7 +336,9 @@ export function DashboardPage() {
|
|||||||
{/* Scan quick access */}
|
{/* Scan quick access */}
|
||||||
<div className="mt-12 flex flex-col sm:flex-row items-center gap-4 sm:gap-6 bg-[#1E8A2A] rounded-3xl p-5 sm:p-6">
|
<div className="mt-12 flex flex-col sm:flex-row items-center gap-4 sm:gap-6 bg-[#1E8A2A] rounded-3xl p-5 sm:p-6">
|
||||||
<div className="flex-1 text-white">
|
<div className="flex-1 text-white">
|
||||||
<h3 className="text-2xl font-bold">Siap Mendeteksi Penyakit Daun?</h3>
|
<h3 className="text-2xl font-bold">
|
||||||
|
Siap Mendeteksi Penyakit Daun?
|
||||||
|
</h3>
|
||||||
<p className="text-sm text-[#9AD872] font-normal mt-2">
|
<p className="text-sm text-[#9AD872] font-normal mt-2">
|
||||||
Unggah foto daun jagung Anda dan dapatkan hasil analisis AI
|
Unggah foto daun jagung Anda dan dapatkan hasil analisis AI
|
||||||
dalam hitungan detik.
|
dalam hitungan detik.
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function DiagnosesPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-emerald-800">Diagnosa Tanaman</h1>
|
<h1 className="text-2xl font-bold text-emerald-800">Diagnosa Penyakit</h1>
|
||||||
<p className="text-gray-500 mt-1 text-md">
|
<p className="text-gray-500 mt-1 text-md">
|
||||||
Lihat hasil diagnosa dari scan yang telah dilakukan
|
Lihat hasil diagnosa dari scan yang telah dilakukan
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export function ExpertReviewsPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-emerald-800">Review Pakar</h1>
|
<h1 className="text-2xl font-bold text-emerald-800">Tinjauan Pakar</h1>
|
||||||
<p className="text-gray-500 mt-1 text-md">
|
<p className="text-gray-500 mt-1 text-md">
|
||||||
Tinjau hasil diagnosa dari scan yang telah dilakukan dan berikan
|
Tinjau hasil diagnosa dari scan yang telah dilakukan dan berikan
|
||||||
feedback untuk meningkatkan akurasi sistem AI ZeaVis Edu
|
feedback untuk meningkatkan akurasi sistem AI ZeaVis Edu
|
||||||
|
|||||||
@@ -36,21 +36,18 @@ export function ScanPage() {
|
|||||||
|
|
||||||
// Camera mode state
|
// Camera mode state
|
||||||
const [useCamera, setUseCamera] = useState(false);
|
const [useCamera, setUseCamera] = useState(false);
|
||||||
const handleCameraCapture = useCallback(
|
const handleCameraCapture = useCallback((file: File) => {
|
||||||
(file: File) => {
|
setFileName(file.name);
|
||||||
setFileName(file.name);
|
const url = URL.createObjectURL(file);
|
||||||
const url = URL.createObjectURL(file);
|
setPreviewUrl(url);
|
||||||
setPreviewUrl(url);
|
|
||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
setImageDimensions({ width: img.width, height: img.height });
|
setImageDimensions({ width: img.width, height: img.height });
|
||||||
};
|
};
|
||||||
img.src = url;
|
img.src = url;
|
||||||
setUseCamera(false);
|
setUseCamera(false);
|
||||||
},
|
}, []);
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: (file: File) => apiClient.createDiagnosis(file),
|
mutationFn: (file: File) => apiClient.createDiagnosis(file),
|
||||||
@@ -107,10 +104,10 @@ export function ScanPage() {
|
|||||||
{/* Main Header */}
|
{/* Main Header */}
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-emerald-800">Scan Tanaman</h1>
|
<h1 className="text-2xl font-bold text-emerald-800">Pindai Daun</h1>
|
||||||
<p className="text-gray-500 mt-1 text-md">
|
<p className="text-gray-500 mt-1 text-md">
|
||||||
Unggah foto daun jagung untuk dianalisis oleh sistem AI kami secara
|
Unggah foto daun jagung untuk dianalisis oleh sistem AI kami secara
|
||||||
real-time.
|
real-time
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
@@ -170,12 +167,16 @@ export function ScanPage() {
|
|||||||
className="w-full border-2 border-dashed border-green-300 rounded-md p-10 h-60 text-center cursor-pointer"
|
className="w-full border-2 border-dashed border-green-300 rounded-md p-10 h-60 text-center cursor-pointer"
|
||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
>
|
>
|
||||||
<Upload className="mx-auto text-green-500 mb-3" size={48} />
|
<Upload
|
||||||
|
className="mx-auto text-green-500 mb-3"
|
||||||
|
size={48}
|
||||||
|
/>
|
||||||
<h3 className="font-semibold text-base text-gray-800 mb-1">
|
<h3 className="font-semibold text-base text-gray-800 mb-1">
|
||||||
Seret & Lepas Foto Daun
|
Seret & Lepas Foto Daun
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-gray-500 mb-3">
|
<p className="text-xs text-gray-500 mb-3">
|
||||||
atau klik untuk memilih file berkas dari perangkat Anda
|
atau klik untuk memilih file berkas dari perangkat
|
||||||
|
Anda
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-2 justify-center">
|
<div className="flex flex-wrap gap-2 justify-center">
|
||||||
<div className="bg-green-100 text-green-700 px-3 py-1 rounded-full inline-flex items-center gap-1 text-xs font-medium">
|
<div className="bg-green-100 text-green-700 px-3 py-1 rounded-full inline-flex items-center gap-1 text-xs font-medium">
|
||||||
|
|||||||
@@ -286,12 +286,11 @@ export function TelemetryPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl md:text-[28px] font-extrabold text-[#214B11] flex items-center gap-3">
|
<h1 className="text-2xl font-bold text-emerald-800">
|
||||||
<Activity className="h-7 w-7 text-[#48A111]" />
|
|
||||||
Telemetry Dashboard
|
Telemetry Dashboard
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground mt-0.5">
|
<p className="text-gray-500 mt-1 text-md">
|
||||||
Real-time metrics from Prometheus
|
Real-time monitoring dari performa sistem dan aplikasi ZeaVis Edu
|
||||||
{error && <span className="text-amber-600 ml-2">(partial — {error})</span>}
|
{error && <span className="text-amber-600 ml-2">(partial — {error})</span>}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,6 +53,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tanstack/react-query": "^5.100.11",
|
"@tanstack/react-query": "^5.100.11",
|
||||||
|
"@tauri-apps/plugin-deep-link": "^2",
|
||||||
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"@zeavis/shared": "workspace:*",
|
"@zeavis/shared": "workspace:*",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user