406 lines
21 KiB
PHP
406 lines
21 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('content')
|
|
<div class="max-w-7xl mx-auto py-12 px-6">
|
|
<div class="flex flex-col md:flex-row justify-between items-center mb-10 gap-4 border-b pb-8">
|
|
<div class="flex items-center gap-4">
|
|
<a href="{{ url()->previous() }}" class="w-10 h-10 bg-white border border-gray-100 rounded-xl flex items-center justify-center shadow-sm hover:bg-orange-50 hover:border-orange-200 transition-all group" title="Kembali"> <svg class="w-5 h-5 text-gray-400 group-hover:text-orange-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
</svg>
|
|
</a>
|
|
<div>
|
|
<h1 class="text-4xl font-black text-gray-900 tracking-tight leading-none uppercase">CITRA <span class="text-orange-700">DIGITAL</span></h1>
|
|
<p class="text-gray-500 mt-1 uppercase text-[10px] font-bold tracking-[0.2em]">Sistem Klasifikasi SVM</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-4">
|
|
<a href="{{ route('history') }}" class="flex items-center gap-2 px-5 py-2.5 bg-gray-900 text-white rounded-xl hover:bg-orange-700 transition-all shadow-lg active:scale-95 group">
|
|
<svg class="w-4 h-4 text-gray-400 group-hover:text-white transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span class="text-[10px] font-black uppercase tracking-widest">Lihat Riwayat</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
|
|
<div class="lg:col-span-3 space-y-4">
|
|
<div class="bg-white border rounded-[2rem] p-6 shadow-sm">
|
|
<h3 class="font-bold text-sm mb-4 uppercase tracking-wider text-gray-400">Prosedur</h3>
|
|
<ul class="space-y-4">
|
|
<li class="flex gap-3">
|
|
<div class="w-6 h-6 rounded-full bg-orange-100 text-orange-700 flex items-center justify-center text-xs font-bold flex-none">1</div>
|
|
<p class="text-xs text-gray-600 leading-relaxed">Letakkan satu biji kopi di atas permukaan <strong>putih polos</strong>.</p>
|
|
</li>
|
|
<li class="flex gap-3">
|
|
<div class="w-6 h-6 rounded-full bg-orange-100 text-orange-700 flex items-center justify-center text-xs font-bold flex-none">2</div>
|
|
<p class="text-xs text-gray-600 leading-relaxed">Pastikan biji berada tepat di dalam <strong>kotak bidik</strong>.</p>
|
|
</li>
|
|
<li class="flex gap-3">
|
|
<div class="w-6 h-6 rounded-full bg-orange-100 text-orange-700 flex items-center justify-center text-xs font-bold flex-none">3</div>
|
|
<p class="text-xs text-gray-600 leading-relaxed">Klik tombol klasifikasi dan tunggu <strong>proses SVM</strong> selesai.</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="bg-white border rounded-[2rem] p-6 shadow-sm">
|
|
<h3 class="font-bold text-sm mb-3 uppercase tracking-wider text-orange-700">Contoh Citra Ideal (Dataset)</h3>
|
|
<div class="space-y-3">
|
|
<div class="w-full aspect-square rounded-2xl overflow-hidden border border-gray-100 bg-gray-50 flex items-center justify-center p-2">
|
|
<img src="{{ asset('img/contoh.jpg') }}" alt="Contoh Dataset" class="w-full h-full object-contain rounded-xl">
|
|
</div>
|
|
<div class="bg-orange-50/70 rounded-xl p-3 border border-orange-100">
|
|
<p class="text-[11px] text-orange-800 leading-relaxed font-medium">
|
|
<strong>💡 Tips Karakteristik Webcam:</strong> Jika gambar dari webcam Anda terlihat agak redup/gelap, dekatkan lampu eksternal (seperti lampu belajar atau flash HP) ke arah alas putih agar distribusi warna piksel mendekati contoh gambar dataset di atas.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gray-900 rounded-[2.5rem] p-6 text-white overflow-hidden relative group">
|
|
<div class="relative z-10 space-y-3">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-2 h-2 bg-orange-500 rounded-full animate-pulse"></div>
|
|
<p class="text-[10px] font-black uppercase tracking-[0.2em] text-orange-500">Standarisasi Digital</p>
|
|
</div>
|
|
<p class="text-sm font-bold leading-snug">Deteksi warna dilakukan secara objektif oleh sistem pintar.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lg:col-span-6">
|
|
<div class="relative">
|
|
<div class="relative bg-black rounded-[3rem] overflow-hidden aspect-square border-[8px] border-white shadow-2xl ring-1 ring-gray-200">
|
|
<video id="video" class="w-full h-full object-cover" autoplay playsinline></video>
|
|
<div id="scanner-line" class="hidden absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-transparent via-orange-500 to-transparent shadow-[0_0_15px_rgba(249,115,22,0.8)] z-30 animate-scan"></div>
|
|
|
|
<div class="absolute inset-0 flex items-center justify-center pointer-events-none z-20">
|
|
<div class="w-48 h-48 border border-white/30 rounded-3xl flex items-center justify-center relative">
|
|
<div class="absolute -top-1 -left-1 w-6 h-6 border-t-4 border-l-4 border-orange-500 rounded-tl-lg"></div>
|
|
<div class="absolute -top-1 -right-1 w-6 h-6 border-t-4 border-r-4 border-orange-500 rounded-tr-lg"></div>
|
|
<div class="absolute -bottom-1 -left-1 w-6 h-6 border-b-4 border-l-4 border-orange-500 rounded-bl-lg"></div>
|
|
<div class="absolute -bottom-1 -right-1 w-6 h-6 border-b-4 border-r-4 border-orange-500 rounded-br-lg"></div>
|
|
<div class="w-1 h-1 bg-orange-500 rounded-full shadow-[0_0_8px_#f97316]"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="capture-area" class="absolute -bottom-6 inset-x-0 flex flex-col items-center gap-3 z-40 transition-all duration-500">
|
|
|
|
<!-- Tombol Webcam -->
|
|
<button id="capture" class="group flex items-center gap-3 bg-white px-8 py-4 rounded-full shadow-xl border border-gray-100 hover:bg-orange-700 hover:text-white transition-all duration-300 active:scale-95">
|
|
|
|
<div class="w-10 h-10 bg-orange-700 group-hover:bg-white rounded-full flex items-center justify-center transition-colors">
|
|
<svg class="w-5 h-5 text-white group-hover:text-orange-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z">
|
|
</path>
|
|
</svg>
|
|
</div>
|
|
|
|
<span class="font-bold uppercase tracking-widest text-sm text-gray-900 group-hover:text-white">
|
|
Jalankan Klasifikasi
|
|
</span>
|
|
</button>
|
|
|
|
<!-- Tombol Upload -->
|
|
<label class="group flex items-center gap-2 bg-gray-100 px-6 py-3 rounded-full border border-gray-200 hover:bg-orange-700 hover:text-white transition-all duration-300 cursor-pointer shadow-md">
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4-4m0 0l-4 4m4-4v12"/>
|
|
</svg>
|
|
|
|
<span class="font-bold uppercase tracking-widest text-[11px]">
|
|
Upload Gambar
|
|
</span>
|
|
|
|
<input type="file" id="upload-image" accept="image/*" class="hidden">
|
|
</label>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lg:col-span-3">
|
|
<div id="result-area" class="h-full">
|
|
<div class="bg-white rounded-[2.5rem] border p-8 flex flex-col h-full shadow-sm relative overflow-hidden transition-all duration-500">
|
|
<div id="placeholder" class="m-auto text-center space-y-4">
|
|
<div class="w-16 h-16 bg-gray-50 rounded-2xl flex items-center justify-center mx-auto border border-dashed text-gray-300">
|
|
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg>
|
|
</div>
|
|
<p class="text-xs text-gray-400 font-medium tracking-wide">Menunggu input gambar...</p>
|
|
</div>
|
|
|
|
<div id="actual-result" class="hidden space-y-6 animate-fadeIn">
|
|
<div class="text-center">
|
|
<p class="text-[10px] font-black text-orange-700 uppercase tracking-[0.3em] mb-1">Hasil Prediksi</p>
|
|
<h2 id="prediction-text" class="text-4xl font-black text-gray-900 leading-none italic uppercase">MEDIUM</h2>
|
|
<div class="mt-3 flex items-center justify-center gap-2 text-gray-400">
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
|
<p id="result-date" class="text-[9px] font-bold uppercase tracking-wider"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div class="w-full aspect-square rounded-3xl overflow-hidden border-4 border-gray-50 shadow-inner bg-gray-100">
|
|
<canvas id="canvas" class="w-full h-full object-cover"></canvas>
|
|
</div>
|
|
|
|
<div class="bg-gray-50 rounded-2xl p-4 space-y-3 border border-gray-100">
|
|
<div class="flex justify-between text-[10px] font-black uppercase text-gray-400">
|
|
<span>Confidence Score</span>
|
|
<span class="text-gray-900" id="confidence">0%</span>
|
|
</div>
|
|
<div class="h-1.5 w-full bg-gray-200 rounded-full overflow-hidden">
|
|
<div id="confidence-bar" class="h-full bg-orange-600 transition-all duration-1000" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="action-area" class="space-y-3">
|
|
<button id="save-btn" class="w-full py-4 bg-orange-700 text-white rounded-2xl text-[10px] font-black transition-all uppercase tracking-[0.2em] shadow-lg flex items-center justify-center gap-2">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path></svg>
|
|
Simpan ke Riwayat
|
|
</button>
|
|
<button id="reset-btn" class="w-full py-4 bg-gray-100 text-gray-500 hover:text-gray-700 rounded-2xl text-[10px] font-black transition-all uppercase tracking-[0.2em] border border-gray-200">
|
|
↺ Abaikan & Analisis Ulang
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
@keyframes scan { 0% { top: 0%; opacity: 0; } 10% { opacity: 1; } 90% { opacity: 1; } 100% { top: 100%; opacity: 0; } }
|
|
.animate-scan { animation: scan 2s linear infinite; }
|
|
.animate-fadeIn { animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
|
</style>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const video = document.getElementById('video');
|
|
const captureArea = document.getElementById('capture-area');
|
|
const captureBtn = document.getElementById('capture');
|
|
const scannerLine = document.getElementById('scanner-line');
|
|
const placeholder = document.getElementById('placeholder');
|
|
const actualResult = document.getElementById('actual-result');
|
|
const canvas = document.getElementById('canvas');
|
|
|
|
const predictionText = document.getElementById('prediction-text');
|
|
const confidenceLabel = document.getElementById('confidence');
|
|
const confidenceBar = document.getElementById('confidence-bar');
|
|
const resultDate = document.getElementById('result-date');
|
|
|
|
const actionArea = document.getElementById('action-area');
|
|
|
|
let currentTempData = null;
|
|
|
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
navigator.mediaDevices.getUserMedia({
|
|
video: true
|
|
})
|
|
.then(function(stream) {
|
|
video.srcObject = stream;
|
|
video.play();
|
|
})
|
|
.catch(err => {
|
|
console.error("Kamera Error:", err);
|
|
alert("Kamera tidak ditemukan.");
|
|
});
|
|
}
|
|
|
|
function handleIgnore() {
|
|
if (currentTempData) {
|
|
fetch("{{ route('api.destroy.temp') }}", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": "{{ csrf_token() }}" },
|
|
body: JSON.stringify({ image_name: currentTempData.image_name })
|
|
});
|
|
}
|
|
resetToScanMode();
|
|
}
|
|
|
|
function handleSave() {
|
|
const saveBtn = document.getElementById('save-btn');
|
|
saveBtn.disabled = true;
|
|
saveBtn.innerText = "Menyimpan...";
|
|
|
|
fetch("{{ route('api.save') }}", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": "{{ csrf_token() }}" },
|
|
body: JSON.stringify({
|
|
image_name: currentTempData.image_name,
|
|
label: currentTempData.label,
|
|
confidence: currentTempData.confidence
|
|
})
|
|
})
|
|
.then(res => res.json())
|
|
.then(res => {
|
|
actionArea.innerHTML = `
|
|
<button id="scan-lagi-btn" class="w-full py-4 bg-green-600 text-white rounded-2xl text-[10px] font-black transition-all uppercase tracking-[0.2em] hover:bg-green-700 shadow-lg animate-fadeIn flex items-center justify-center gap-2">
|
|
<span>Berhasil Tersimpan ✅</span>
|
|
</button>
|
|
`;
|
|
document.getElementById('scan-lagi-btn').onclick = resetToScanMode;
|
|
});
|
|
}
|
|
|
|
function resetToScanMode() {
|
|
actualResult.classList.add('hidden');
|
|
placeholder.classList.remove('hidden');
|
|
captureArea.classList.remove('hidden', 'opacity-0');
|
|
|
|
currentTempData = null;
|
|
actionArea.innerHTML = `
|
|
<button id="save-btn" class="w-full py-4 bg-orange-700 text-white rounded-2xl text-[10px] font-black transition-all uppercase tracking-[0.2em] shadow-lg flex items-center justify-center gap-2">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path></svg>
|
|
Simpan ke Riwayat
|
|
</button>
|
|
<button id="reset-btn" class="w-full py-4 bg-gray-100 text-gray-500 hover:text-gray-700 rounded-2xl text-[10px] font-black transition-all uppercase tracking-[0.2em] border border-gray-200 mt-3">
|
|
↺ Abaikan & Analisis Ulang
|
|
</button>
|
|
`;
|
|
|
|
captureBtn.disabled = false;
|
|
captureBtn.querySelector('span').innerText = "Jalankan Klasifikasi";
|
|
scannerLine.classList.add('hidden');
|
|
|
|
document.getElementById('save-btn').onclick = handleSave;
|
|
document.getElementById('reset-btn').onclick = handleIgnore;
|
|
}
|
|
const uploadInput = document.getElementById('upload-image');
|
|
|
|
uploadInput.addEventListener('change', function(e) {
|
|
|
|
const file = e.target.files[0];
|
|
|
|
if (!file) return;
|
|
|
|
scannerLine.classList.remove('hidden');
|
|
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = function(event) {
|
|
|
|
const img = new Image();
|
|
|
|
img.onload = function() {
|
|
|
|
const context = canvas.getContext('2d');
|
|
|
|
// Samakan preprocessing dengan model
|
|
canvas.width = 224;
|
|
canvas.height = 224;
|
|
|
|
context.drawImage(img, 0, 0, 224, 224);
|
|
|
|
// Convert canvas ke base64
|
|
const finalImage = canvas.toDataURL('image/png');
|
|
|
|
fetch("{{ route('api.classify') }}", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-CSRF-TOKEN": "{{ csrf_token() }}"
|
|
},
|
|
body: JSON.stringify({
|
|
image: finalImage
|
|
})
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
|
|
scannerLine.classList.add('hidden');
|
|
|
|
if (data.status === 'error') {
|
|
alert(data.message);
|
|
return;
|
|
}
|
|
|
|
captureArea.classList.add('hidden', 'opacity-0');
|
|
placeholder.classList.add('hidden');
|
|
actualResult.classList.remove('hidden');
|
|
|
|
predictionText.innerText = data.label;
|
|
confidenceLabel.innerText = data.confidence;
|
|
confidenceBar.style.width = data.confidence;
|
|
resultDate.innerText = data.created_at;
|
|
|
|
currentTempData = data;
|
|
|
|
document.getElementById('save-btn').onclick = handleSave;
|
|
document.getElementById('reset-btn').onclick = handleIgnore;
|
|
})
|
|
.catch(err => {
|
|
console.log(err);
|
|
alert("Gagal upload gambar.");
|
|
scannerLine.classList.add('hidden');
|
|
});
|
|
};
|
|
|
|
img.src = event.target.result;
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
});
|
|
captureBtn.addEventListener('click', function() {
|
|
scannerLine.classList.remove('hidden');
|
|
captureBtn.disabled = true;
|
|
captureBtn.querySelector('span').innerText = "Menganalisis...";
|
|
|
|
const context = canvas.getContext('2d');
|
|
|
|
// Ambil ukuran video
|
|
const vw = video.videoWidth;
|
|
const vh = video.videoHeight;
|
|
|
|
const cropSize = Math.min(vw, vh) * 0.6;
|
|
const startX = (vw - cropSize) / 2;
|
|
const startY = (vh - cropSize) / 2;
|
|
|
|
canvas.width = cropSize;
|
|
canvas.height = cropSize;
|
|
|
|
context.drawImage(video, startX, startY, cropSize, cropSize, 0, 0, cropSize, cropSize);
|
|
|
|
const imageData = canvas.toDataURL('image/png');
|
|
|
|
fetch("{{ route('api.classify') }}", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": "{{ csrf_token() }}" },
|
|
body: JSON.stringify({ image: imageData })
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
scannerLine.classList.add('hidden');
|
|
|
|
captureArea.classList.add('hidden', 'opacity-0');
|
|
placeholder.classList.add('hidden');
|
|
actualResult.classList.remove('hidden');
|
|
|
|
predictionText.innerText = data.label;
|
|
confidenceLabel.innerText = data.confidence;
|
|
confidenceBar.style.width = data.confidence;
|
|
resultDate.innerText = data.created_at;
|
|
|
|
currentTempData = data;
|
|
|
|
document.getElementById('save-btn').onclick = handleSave;
|
|
document.getElementById('reset-btn').onclick = handleIgnore;
|
|
})
|
|
.catch(err => {
|
|
alert("Gagal klasifikasi.");
|
|
captureBtn.disabled = false;
|
|
scannerLine.classList.add('hidden');
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
@endpush |