880 lines
29 KiB
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()">×</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 === ' ' ? ' _ ' : item.letter;
|
|
|
|
span.innerHTML = `${displayChar} <button onclick="removeDetectedItem(${index})" class="word-delete-btn">×</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> |