MIF_E31230580/resources/views/translate/index.blade.php

880 lines
29 KiB
PHP

<x-app-layout>
<!-- Header Halaman -->
<x-slot name="header">
<div class="header-container-css">
<span class="icon-pulse-css">📷</span>
<h2 class="title-grad-css">Latihan Bahasa Isyarat BISINDO</h2>
</div>
</x-slot>
<!-- STYLE LEMBARAN CSS UTUH (MURNI CUSTOM CSS - BEBAS DEPENDENSI TAILWIND) -->
<style>
/* 1. VARIABEL WARNA & TEMA PREMIUM */
:root {
--primary-blue: #0052cc;
--primary-glow: rgba(0, 82, 204, 0.15);
--neon-sky: #00b3ff;
--light-ice: #f2f7ff;
--clean-white: #ffffff;
--text-main: #1e293b;
--text-muted: #64748b;
--border-soft: #e2e8f0;
--card-shadow: 0 10px 30px rgba(0, 0, 0, 0.03);
--grad-banner: linear-gradient(135deg, #0f172a 0%, #1e3a8a 50%, #0052cc 100%);
--grad-accent: linear-gradient(135deg, #0052cc 0%, #00b3ff 100%);
--success-green: #059669;
}
/* 2. BASE WRAPPER & GELEMBUNG LATAR BELAKANG */
.tr-wrapper-css {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: #f8fafc;
min-height: 100vh;
padding: 40px 20px;
position: relative;
overflow: hidden;
box-sizing: border-box;
}
.tr-container-css {
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 30px;
position: relative;
z-index: 10;
}
/* Floating Blobs Background */
.blob-css {
position: absolute;
border-radius: 50%;
filter: blur(80px);
pointer-events: none;
z-index: 1;
opacity: 0.25;
}
.blob-1 {
width: 400px;
height: 400px;
background-color: #93c5fd;
top: 5%;
left: -10%;
animation: float-slow 8s infinite ease-in-out;
}
.blob-2 {
width: 500px;
height: 500px;
background-color: #bae6fd;
bottom: 5%;
right: -10%;
animation: float-slow-rev 10s infinite ease-in-out;
}
/* 3. GRID UTAMA */
.tr-grid-css {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 30px;
}
@media (max-width: 1024px) {
.tr-grid-css {
grid-template-columns: 1fr;
}
}
/* 4. PREMIUM CARDS (3D GLOW EFFECTS) */
.tr-card-css {
background: var(--clean-white);
border: 1px solid var(--border-soft);
border-radius: 32px;
padding: 30px;
box-shadow: var(--card-shadow);
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
}
.tr-card-title {
font-size: 18px;
font-weight: 900;
color: var(--text-main);
margin: 0 0 4px 0;
letter-spacing: -0.5px;
}
.tr-card-subtitle {
font-size: 11px;
line-height: 1.6;
color: var(--text-muted);
margin: 0 0 20px 0;
}
/* 5. WEBCAM MONITOR FRAME (FUTURISTIC AI STYLING) */
.camera-container-css {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
background-color: #020617;
border-radius: 24px;
overflow: hidden;
border: 2px solid var(--border-soft);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.camera-container-css video {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Overlays & Ornamen AI (Pojok Kotak Mengambang) */
.camera-overlay-css {
position: absolute;
inset: 0;
pointer-events: none;
border: 20px solid transparent;
z-index: 5;
}
/* Mockup Bidik Sudut Kamera (Pindai AI) */
.camera-overlay-css::before,
.camera-overlay-css::after {
content: '';
position: absolute;
width: 30px;
height: 30px;
border: 3px solid var(--neon-sky);
}
.camera-overlay-css::before {
top: 0;
left: 0;
border-right: none;
border-bottom: none;
}
.camera-overlay-css::after {
bottom: 0;
right: 0;
border-left: none;
border-top: none;
}
/* Status Mati Kamera */
.no-camera-state-css {
position: absolute;
inset: 0;
background-color: #0f172a;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
color: #94a3b8;
z-index: 4;
padding: 24px;
text-align: center;
}
.no-camera-state-css span:first-child {
font-size: 40px;
animation: float-slow 4s infinite ease-in-out;
}
.no-camera-state-css span:last-child {
font-size: 11px;
max-width: 250px;
line-height: 1.5;
}
/* 6. TOMBOL NAVIGASI KAMERA & AKSI */
.btn-group-css {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 20px;
}
.btn-css {
font-family: inherit;
border: none;
border-radius: 14px;
padding: 12px 24px;
font-size: 11px;
font-weight: 800;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.02);
}
.btn-css:hover {
transform: translateY(-2px);
}
.btn-primary-blue {
background: var(--grad-accent);
color: var(--clean-white);
box-shadow: 0 8px 20px rgba(0, 82, 204, 0.25);
}
.btn-primary-blue:hover {
box-shadow: 0 12px 25px rgba(0, 179, 255, 0.35);
}
.btn-secondary {
background-color: #e2e8f0;
color: #475569;
}
.btn-secondary:hover {
background-color: #cbd5e1;
color: #1e293b;
}
.btn-success {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: var(--clean-white);
box-shadow: 0 8px 20px rgba(16, 185, 129, 0.25);
}
.btn-success:hover {
box-shadow: 0 12px 25px rgba(5, 150, 105, 0.35);
}
/* Tombol Riwayat Samping */
.btn-history-trigger {
background-color: var(--light-ice);
border: 1px solid rgba(0, 82, 204, 0.15);
color: var(--primary-blue);
}
.btn-history-trigger:hover {
background-color: var(--primary-blue);
color: var(--clean-white);
}
/* 7. REKONSTRUKSI TEKS DISPLAY PANEL */
.phrase-display-css {
background-color: var(--light-ice);
border: 1px solid rgba(0, 82, 204, 0.1);
border-radius: 20px;
min-height: 180px;
padding: 24px;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
gap: 10px;
position: relative;
}
.empty-alert-css {
font-size: 11px;
color: var(--text-muted);
font-style: italic;
}
/* Badge Kata Isyarat */
.word-badge-css {
background-color: var(--clean-white);
border: 1px solid var(--border-soft);
color: var(--primary-blue);
font-size: 11px;
font-weight: 800;
padding: 8px 14px;
border-radius: 10px;
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 10px rgba(0, 82, 204, 0.04);
animation: bounce-slow 2s infinite ease-in-out;
}
.word-delete-btn {
background: transparent;
border: none;
color: #ef4444;
font-weight: 900;
font-size: 14px;
cursor: pointer;
padding: 0;
line-height: 1;
}
.word-delete-btn:hover {
color: #b91c1c;
}
.clear-link-css {
background: transparent;
border: none;
color: #ef4444;
font-size: 11px;
font-weight: 800;
cursor: pointer;
padding: 0;
transition: color 0.2s ease;
}
.clear-link-css:hover {
color: #b91c1c;
text-decoration: underline;
}
/* 8. PANEL SIMPAN DATABASE (BOTTOM ACTION) */
.save-panel-css {
border-top: 1px solid var(--light-ice);
padding-top: 24px;
margin-top: 24px;
}
.save-title-css {
font-size: 13px;
font-weight: 850;
color: var(--text-main);
margin: 0 0 4px 0;
}
.save-desc-css {
font-size: 11px;
color: var(--text-muted);
line-height: 1.6;
margin: 0 0 16px 0;
}
.btn-full-width-css {
width: 100%;
justify-content: center;
padding: 16px 32px;
border-radius: 16px;
font-size: 12px;
font-weight: 900;
background: var(--grad-banner);
color: var(--clean-white);
border: none;
cursor: pointer;
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.15);
transition: all 0.3s ease;
}
.btn-full-width-css:hover {
transform: translateY(-2px);
box-shadow: 0 15px 30px rgba(15, 23, 42, 0.25);
}
/* ================= 9. SLIDING SIDEBAR DRAWER CSS ================= */
.drawer-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.2);
backdrop-filter: blur(4px);
z-index: 1999;
opacity: 0;
pointer-events: none;
transition: opacity 0.4s ease;
}
.drawer-overlay.active {
opacity: 1;
pointer-events: auto;
}
.sidebar-drawer {
position: fixed;
top: 0;
right: -420px;
width: 420px;
height: 100vh;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(12px);
box-shadow: -10px 0 30px rgba(0, 82, 204, 0.08);
z-index: 2000;
transition: right 0.4s cubic-bezier(0.16, 1, 0.3, 1);
padding: 30px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 20px;
border-left: 1px solid var(--border-soft);
}
@media (max-width: 480px) {
.sidebar-drawer {
width: 100%;
right: -100%;
}
}
.sidebar-drawer.active {
right: 0;
}
.drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-soft);
padding-bottom: 20px;
}
.drawer-title {
font-size: 16px;
font-weight: 900;
color: var(--text-main);
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.drawer-close-btn {
background: transparent;
border: none;
font-size: 20px;
font-weight: 900;
color: var(--text-muted);
cursor: pointer;
padding: 5px;
line-height: 1;
transition: color 0.2s ease;
}
.drawer-close-btn:hover {
color: #ef4444;
}
.drawer-content {
flex-grow: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 14px;
padding-right: 5px;
}
/* Custom scrollbar */
.drawer-content::-webkit-scrollbar {
width: 5px;
}
.drawer-content::-webkit-scrollbar-thumb {
background-color: var(--border-soft);
border-radius: 100px;
}
/* Kartu Riwayat Samping */
.drawer-item-card {
background-color: var(--clean-white);
border: 1px solid var(--border-soft);
border-radius: 16px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 6px;
transition: all 0.3s ease;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.01);
}
.drawer-item-card:hover {
border-color: var(--primary-blue);
background-color: var(--light-ice);
transform: translateY(-2px);
}
.drawer-item-text {
font-size: 13px;
font-weight: 800;
color: var(--text-main);
margin: 0;
line-height: 1.4;
}
.drawer-item-date {
font-size: 9px;
font-weight: 700;
color: var(--text-muted);
}
/* 10. ANIMASI KEYFRAMES */
@keyframes float-slow {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-12px) scale(1.03); }
}
@keyframes float-slow-rev {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(12px) scale(0.97); }
}
@keyframes bounce-slow {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
/* Overriding Slot Header Navigation Styles */
.header-container-css {
display: flex;
align-items: center;
gap: 12px;
}
.icon-pulse-css {
font-size: 24px;
animation: bounce-slow 2s infinite ease-in-out;
}
.title-grad-css {
font-size: 20px;
font-weight: 900;
background: linear-gradient(to right, var(--primary-blue), var(--neon-sky));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin: 0;
letter-spacing: -0.5px;
}
</style>
<!-- WRAPPER UTAMA PENERJEMAH KAMERA -->
<div class="tr-wrapper-css">
<!-- Blobs Background -->
<div class="blob-css blob-1"></div>
<div class="blob-css blob-2"></div>
<div class="tr-container-css">
<!-- KOTAK INTERACTIVE MAIN CARD -->
<div class="tr-card-css">
<div class="tr-grid-css">
<!-- SISI KIRI: INPUT MONITOR WEBCAM -->
<div>
<h3 class="tr-card-title">Input Kamera Real-Time</h3>
<p class="tr-card-subtitle">Coba di ruang bercahaya terang agar sistem CNN & MediaPipe mengenali koordinat ruas jari tangan kamu.</p>
<!-- Bingkai Monitor Kamera AI -->
<div class="camera-container-css">
<video id="webcam" autoplay playsinline></video>
<div class="camera-overlay-css"></div>
<!-- Status Mati Kamera -->
<div id="no-camera-state" class="no-camera-state-css">
<span>🎥</span>
<span>Kamera dinonaktifkan. Silakan klik tombol di bawah untuk menyalakan izin deteksi.</span>
</div>
</div>
<!-- Tombol Kendali Aksi Kamera -->
<div class="btn-group-css">
<button onclick="startCamera()" class="btn-css btn-primary-blue">Nyalakan Kamera</button>
<button onclick="stopCamera()" class="btn-css btn-secondary">Matikan Kamera</button>
<button id="btn-deteksi" onclick="startCaptureWithTimer()" class="btn-css btn-success">
Ambil Gambar & Deteksi (Abjad Bisindo)
</button>
<!-- TAMBAHKAN TOMBOL SPASI DI SINI -->
<button onclick="addSpace()" class="btn-css" style="background: #64748b; color: white;">Spasi ( _ )</button>
</div>
</div>
<!-- SISI KANAN: PANEL SUSUNAN REKONSTRUKSI KALIMAT -->
<div style="display: flex; flex-direction: column; justify-content: space-between;">
<div>
<h3 class="tr-card-title">Hasil Rekonstruksi Susunan Huruf</h3>
<p class="tr-card-subtitle">Kumpulan huruf yang berhasil dibaca oleh model CNN.</p>
<!-- Kotak Penampil Kata -->
<div id="phrase-display" class="phrase-display-css">
<span id="empty-alert" class="empty-alert-css">Belum ada aktivitas deteksi isyarat tangan...</span>
</div>
<div style="margin-top: 10px; text-align: right;">
<button onclick="resetPhrase()" class="clear-link-css">Hapus Semua Huruf</button>
</div>
</div>
<div class="mt-3 p-3 bg-gray-50 border border-gray-200 rounded-lg flex items-center justify-between">
<span class="text-sm text-gray-600 font-medium">Akurasi prediction:</span>
<h4 id="confidence-score" class="text-sm font-bold text-blue-600 bg-blue-50 px-3 py-1 rounded-md border border-blue-100 mb-0">0.00%</h4>
</div>
<div class="mt-3 p-3 bg-gray-50 border border-gray-200 rounded-lg flex items-center justify-between">
<span class="text-sm text-gray-600 font-medium">Waktu Deteksi:</span>
<h4 id="latency-score" class="text-sm font-bold text-blue-600 bg-blue-50 px-3 py-1 rounded-md border border-blue-100 mb-0">0 ms</h4>
</div>
<!-- Bagian Simpan Hasil ke database -->
<div class="save-panel-css">
<h4 class="save-title-css">Simpan Hasil?</h4>
<p class="save-desc-css">Anda dapat menyimpan susunan kata di atas ke dalam catatan riwayat akun Anda.</p>
<button id="save-btn" onclick="sendToDatabase()" class="btn-full-width-css">
Simpan Riwayat Terjemahan
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ================= SLIDING SIDEBAR DRAWER HTML ================= -->
<div class="drawer-overlay" id="drawer-overlay" onclick="toggleDrawer()"></div>
<div class="sidebar-drawer" id="sidebar-drawer">
<div class="drawer-header">
<h4 class="drawer-title">
<span>🕒</span> Riwayat Terjemahan Anda
</h4>
<button class="drawer-close-btn" onclick="toggleDrawer()">&times;</button>
</div>
<!-- Wadah List Riwayat Samping -->
<div class="drawer-content" id="drawer-history-list">
@if($histories->isEmpty())
<p class="empty-alert-css" id="sidebar-empty-state">Belum ada catatan riwayat terjemahan kamera.</p>
@else
@foreach($histories as $data)
<div class="drawer-item-card">
{{-- TAMBAHKAN BAGIAN INI SUPAYA GAMBARNYA MUNCUL --}}
@if($data->gambar)
<img src="{{ asset($data->gambar) }}" style="width: 100%; height: 120px; object-fit: cover; border-radius: 8px; margin-bottom: 6px;" alt="Foto Isyarat">
@endif
<p class="drawer-item-text">"{{ $data->hasil_teks }}"</p>
<span class="drawer-item-date">{{ $data->created_at->format('d M Y, H:i') }} ({{ $data->created_at->diffForHumans() }})</span>
</div>
@endforeach
@endif
</div>
<!-- SCRIPT FRONTEND JAVASCRIPT -->
<script>
let streamObj = null;
const videoEl = document.getElementById('webcam');
const emptyAlertEl = document.getElementById('empty-alert');
const phraseDisplayEl = document.getElementById('phrase-display');
const historyListEl = document.getElementById('drawer-history-list');
let detectedItems = [];
function toggleDrawer() {
document.getElementById('sidebar-drawer').classList.toggle('active');
document.getElementById('drawer-overlay').classList.toggle('active');
}
async function startCamera() {
try {
streamObj = await navigator.mediaDevices.getUserMedia({ video: true });
videoEl.srcObject = streamObj;
document.getElementById('no-camera-state').style.display = 'none';
} catch (e) {
alert("Tidak dapat mengakses kamera laptop: " + e.message);
}
}
function stopCamera() {
if (streamObj) {
streamObj.getTracks().forEach(track => track.stop());
videoEl.srcObject = null;
document.getElementById('no-camera-state').style.display = 'flex';
}
}
async function triggerSimulate() {
if (!streamObj) {
alert("Silakan nyalakan kamera terlebih dahulu!");
return;
}
let canvas = document.createElement('canvas');
canvas.width = videoEl.videoWidth || 640;
canvas.height = videoEl.videoHeight || 480;
let ctx = canvas.getContext('2d');
ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height);
canvas.toBlob(async function(blob) {
let formData = new FormData();
formData.append('image', blob, 'capture.jpg');
try {
let response = await fetch('http://localhost:5000/predict', {
method: 'POST',
body: formData
});
let data = await response.json();
// Selalu update waktu deteksi (latency) terlepas dari status sukses/gagal
const latencyEl = document.getElementById('latency-score');
if (latencyEl && data.latency) {
latencyEl.innerText = data.latency;
}
if (data.status === 'success') {
detectedItems.push({
letter: data.letter,
blob: blob
});
renderWordsOnly();
const confidenceEl = document.getElementById('confidence-score');
if (confidenceEl) {
confidenceEl.innerText = data.confidence;
}
updateDailyMissionStatus();
} else {
alert("Gagal mendeteksi: " + data.message);
}
} catch (err) {
console.error("Gagal terhubung ke AI:", err);
alert("Pastikan server Python (app.py) sudah dinyalakan di terminal!");
}
}, 'image/jpeg');
}
function addSpace() {
if (detectedItems.length === 0) return;
let lastItem = detectedItems[detectedItems.length - 1];
if (lastItem.letter === ' ') return;
detectedItems.push({
letter: ' ',
blob: null
});
renderWordsOnly();
}
function renderWordsOnly() {
phraseDisplayEl.innerHTML = '';
if (detectedItems.length === 0) {
phraseDisplayEl.innerHTML = `<span id="empty-alert" class="empty-alert-css">Belum ada aktivitas deteksi isyarat tangan...</span>`;
return;
}
detectedItems.forEach((item, index) => {
const span = document.createElement('span');
span.className = "word-badge-css";
let displayChar = item.letter === ' ' ? '&nbsp;_&nbsp;' : item.letter;
span.innerHTML = `${displayChar} <button onclick="removeDetectedItem(${index})" class="word-delete-btn">&times;</button>`;
phraseDisplayEl.appendChild(span);
});
}
function removeDetectedItem(index) {
detectedItems.splice(index, 1);
renderWordsOnly();
}
function resetPhrase() {
detectedItems = [];
renderWordsOnly();
const confidenceEl = document.getElementById('confidence-score');
if (confidenceEl) {
confidenceEl.innerText = "0.00%";
}
const latencyEl = document.getElementById('latency-score');
if (latencyEl) {
latencyEl.innerText = "0 ms";
}
}
async function sendToDatabase() {
if (detectedItems.length === 0) {
alert("Huruf masih kosong, lakukan deteksi terlebih dahulu!");
return;
}
const finalSentence = detectedItems.map(i => i.letter).join('');
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
let formData = new FormData();
formData.append('hasil_teks', finalSentence);
let photoCounter = 0;
detectedItems.forEach((item) => {
if (item.blob !== null) {
formData.append(`images[${photoCounter}]`, item.blob, `char_${photoCounter}.jpg`);
photoCounter++;
}
});
try {
const response = await fetch("{{ route('translate.store') }}", {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken
},
body: formData
});
const result = await response.json();
alert(result.message);
resetPhrase();
window.location.reload();
} catch (error) {
alert("Terjadi kesalahan koneksi ke server.");
console.error(error);
}
}
function updateDailyMissionStatus() {
const iconEl = document.getElementById('quest-2-icon');
if (iconEl) {
iconEl.className = "quest-num-icon success";
iconEl.innerHTML = "✓";
}
const badgeEl = document.getElementById('quest-2-badge');
if (badgeEl) {
badgeEl.className = "quest-status-badge success";
badgeEl.innerHTML = "Selesai";
}
const cardEl = document.getElementById('quest-2-card');
if (cardEl) {
cardEl.classList.remove('active');
}
const progressText = document.getElementById('quest-progress-percent');
if (progressText) {
progressText.innerHTML = "100%";
}
const progressBarFill = document.getElementById('progress-fill-bar');
if (progressBarFill) {
progressBarFill.style.width = "100%";
}
localStorage.setItem('mission_detected_done', 'true');
localStorage.setItem('mission_date', new Date().toDateString());
}
window.addEventListener('DOMContentLoaded', () => {
const savedDate = localStorage.getItem('mission_date');
const today = new Date().toDateString();
if (savedDate !== today) {
localStorage.removeItem('mission_detected_done');
localStorage.setItem('mission_date', today);
} else if (localStorage.getItem('mission_detected_done') === 'true') {
updateDailyMissionStatus();
}
});
function startCaptureWithTimer() {
const btn = document.getElementById('btn-deteksi');
let timer = 3;
btn.disabled = true;
const interval = setInterval(() => {
if (timer > 0) {
btn.innerText = "Tunggu... " + timer;
timer--;
} else {
clearInterval(interval);
btn.innerText = "Ambil Gambar & Deteksi (Simulasi AI)";
btn.disabled = false;
triggerSimulate();
}
}, 1000);
}
</script>
</x-app-layout>