feat(web): refresh simulator dashboard UI
This commit is contained in:
@@ -10,6 +10,17 @@
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/components/ui/select/index.js';
|
||||
import { Switch } from '$lib/components/ui/switch/index.js';
|
||||
import {
|
||||
VideoOff,
|
||||
RefreshCw,
|
||||
Zap,
|
||||
Square,
|
||||
Activity,
|
||||
Settings2,
|
||||
Terminal,
|
||||
Camera
|
||||
} from '@lucide/svelte';
|
||||
|
||||
let cameraVideoElement: HTMLVideoElement | null = null;
|
||||
|
||||
@@ -40,7 +51,7 @@
|
||||
<AppChrome pageKey="camera">
|
||||
<section id="screen-home-camera" class="mx-auto flex h-full min-h-0 max-w-7xl flex-col gap-4 lg:gap-6">
|
||||
<div class="flex flex-col gap-4 lg:gap-6 xl:flex-1 xl:min-h-0">
|
||||
<Card class="glass-card relative flex min-h-[220px] overflow-hidden rounded-3xl border-white/10 bg-[#16161d]/80 sm:min-h-[260px] xl:flex-[3]">
|
||||
<Card variant="glass" class="relative flex min-h-[220px] overflow-hidden sm:min-h-[260px] xl:flex-[3]">
|
||||
<div id="cameraPreview" class="flex-1 bg-black relative flex items-center justify-center {$appState.isMotionActive ? 'bg-red-900/20' : ''}">
|
||||
<video
|
||||
id="cameraVideo"
|
||||
@@ -52,10 +63,10 @@
|
||||
></video>
|
||||
<Badge
|
||||
variant="destructive"
|
||||
class="absolute top-4 left-4 z-20 gap-2 rounded-full border border-white/10 bg-black/50 px-3 py-1.5 text-white backdrop-blur"
|
||||
class="absolute top-4 left-4 z-20 gap-2 rounded-full border border-white/10 bg-black/50 px-3 py-1.5 backdrop-blur"
|
||||
>
|
||||
<span class="w-2.5 h-2.5 bg-red-500 rounded-full shadow-[0_0_8px_rgba(239,68,68,0.8)] animate-pulse"></span>
|
||||
<span class="text-xs font-medium tracking-wide">REC</span>
|
||||
<span class="w-2 h-2 bg-red-500 rounded-full shadow-[0_0_8px_rgba(239,68,68,0.8)] animate-pulse"></span>
|
||||
<span class="text-[10px] font-bold tracking-widest">REC</span>
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -63,27 +74,20 @@
|
||||
id="cameraOfflineOverlay"
|
||||
class="absolute inset-0 bg-[#0a0a0c]/80 backdrop-blur-sm z-10 flex flex-col items-center justify-center gap-4 {$appState.socketConnected ? 'hidden' : ''}"
|
||||
>
|
||||
<Alert class="w-full max-w-sm border-white/10 bg-[#141418]/90 text-center shadow-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-16 w-16 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
|
||||
/>
|
||||
</svg>
|
||||
<AlertTitle class="justify-self-center text-base text-white">Camera Offline</AlertTitle>
|
||||
<AlertDescription class="justify-self-center text-sm text-gray-400">
|
||||
<Alert variant="default" class="w-full max-w-sm border-white/10 bg-[#141418]/90 text-center shadow-xl">
|
||||
<VideoOff class="mx-auto h-12 w-12 text-gray-600 mb-2" />
|
||||
<AlertTitle class="text-base text-white">Camera Offline</AlertTitle>
|
||||
<AlertDescription class="text-sm text-gray-400">
|
||||
Reconnect this device to resume live monitoring.
|
||||
</AlertDescription>
|
||||
<div class="mt-2 flex justify-center">
|
||||
<div class="mt-4 flex justify-center">
|
||||
<Button
|
||||
id="cameraGoOnlineBtn"
|
||||
variant="outline"
|
||||
class="rounded-xl border-green-500/50 text-green-400 hover:border-green-400 hover:bg-green-500/10"
|
||||
onclick={() => appController.goOnline()}
|
||||
>
|
||||
Go Online
|
||||
id="cameraGoOnlineBtn"
|
||||
variant="outline"
|
||||
class="rounded-xl border-emerald-500/50 text-emerald-400 hover:border-emerald-400 hover:bg-emerald-500/10"
|
||||
onclick={() => appController.goOnline()}
|
||||
>
|
||||
Go Online
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
@@ -91,9 +95,10 @@
|
||||
</Card>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:gap-6 xl:min-h-0 xl:flex-[2] xl:grid-cols-[2fr_1fr]">
|
||||
<Card class="glass-card min-h-[220px] overflow-hidden rounded-3xl border-white/10 bg-[#16161d]/80 xl:min-h-0">
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-xs font-bold uppercase tracking-wider text-gray-500">Logs</CardTitle>
|
||||
<Card variant="glass" class="min-h-[220px] overflow-hidden xl:min-h-0">
|
||||
<CardHeader class="pb-2 flex flex-row items-center gap-2">
|
||||
<Terminal class="size-3.5 text-gray-500" />
|
||||
<CardTitle class="text-[10px] font-bold uppercase tracking-widest text-gray-500">Activity Logs</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="flex-1 xl:min-h-0">
|
||||
<ScrollArea
|
||||
@@ -104,39 +109,46 @@
|
||||
<div class="text-gray-600 italic">Awaiting connection...</div>
|
||||
{:else}
|
||||
{#each $appState.activityLog as log (log.id)}
|
||||
<div>[{new Date(log.createdAt).toLocaleTimeString()}] {log.type}: {log.message}</div>
|
||||
<div class="mb-1 leading-relaxed">
|
||||
<span class="text-gray-600">[{new Date(log.createdAt).toLocaleTimeString()}]</span>
|
||||
<span class="text-blue-400/80">{log.type}:</span>
|
||||
<span>{log.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card class="glass-card min-h-[220px] overflow-hidden rounded-3xl border-white/10 bg-[#16161d]/80 xl:min-h-0">
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-xs font-bold uppercase tracking-wider text-gray-500">Actions / Options</CardTitle>
|
||||
<Card variant="glass" class="min-h-[220px] overflow-hidden xl:min-h-0">
|
||||
<CardHeader class="pb-2 flex flex-row items-center gap-2">
|
||||
<Settings2 class="size-3.5 text-gray-500" />
|
||||
<CardTitle class="text-[10px] font-bold uppercase tracking-widest text-gray-500">Control Plane</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4 xl:min-h-0 xl:overflow-y-auto">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Label for="cameraInputSelect" class="text-[11px] font-semibold uppercase tracking-wider text-gray-400">
|
||||
Camera Input
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Label for="cameraInputSelect" class="text-[10px] font-bold uppercase tracking-widest text-gray-400 flex items-center gap-1.5">
|
||||
<Camera class="size-3" />
|
||||
Camera Input
|
||||
</Label>
|
||||
<Button
|
||||
id="refreshCameraInputsBtn"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
class="rounded-lg text-gray-400 hover:text-white"
|
||||
onclick={() => appController.refreshCameraInputs()}
|
||||
>
|
||||
Refresh
|
||||
id="refreshCameraInputsBtn"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
class="h-6 rounded-lg text-gray-500 hover:text-white"
|
||||
onclick={() => appController.refreshCameraInputs()}
|
||||
>
|
||||
<RefreshCw class="size-3 mr-1" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
type="single"
|
||||
value={$appState.selectedCameraInputId}
|
||||
items={cameraInputItems()}
|
||||
onValueChange={(value) => appController.selectCameraInput(value)}
|
||||
>
|
||||
type="single"
|
||||
value={$appState.selectedCameraInputId}
|
||||
items={cameraInputItems()}
|
||||
onValueChange={(value) => appController.selectCameraInput(value)}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="cameraInputSelect"
|
||||
class="h-10 w-full rounded-xl border-white/10 bg-black/40 text-gray-200"
|
||||
@@ -150,20 +162,21 @@
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 rounded-2xl border border-white/10 bg-black/30 p-3 sm:p-4 sm:space-y-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-wider text-gray-400">Automatic Detection</p>
|
||||
<p class="text-sm text-gray-200">
|
||||
{$appState.motionDetection.enabled ? 'Armed for automatic motion events.' : 'Paused until manually armed.'}
|
||||
<p class="text-[10px] font-bold uppercase tracking-widest text-gray-400">Automatic Detection</p>
|
||||
<p class="text-sm text-gray-200 font-medium">
|
||||
{$appState.motionDetection.enabled ? 'Armed' : 'Disarmed'}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
Designed for a plugged-in browser kept in the foreground.
|
||||
<p class="text-[10px] text-gray-500 leading-tight">
|
||||
Foreground browser monitoring.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant={$appState.motionDetection.enabled ? 'secondary' : 'outline'}
|
||||
class="min-w-[112px] self-start rounded-xl sm:self-auto"
|
||||
class="min-w-[80px] h-9 self-start rounded-xl sm:self-auto text-xs"
|
||||
onclick={() => appController.setMotionDetectionEnabled(!$appState.motionDetection.enabled)}
|
||||
>
|
||||
{$appState.motionDetection.enabled ? 'Pause' : 'Arm'}
|
||||
@@ -172,10 +185,10 @@
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Label for="motionDetectionProfile" class="text-[11px] font-semibold uppercase tracking-wider text-gray-400">
|
||||
Detection Profile
|
||||
<Label for="motionDetectionProfile" class="text-[10px] font-bold uppercase tracking-widest text-gray-400">
|
||||
Profile
|
||||
</Label>
|
||||
<span class="text-[11px] uppercase tracking-wider text-gray-500">{$appState.motionDetection.state}</span>
|
||||
<span class="text-[10px] font-bold uppercase tracking-widest text-blue-500/80">{$appState.motionDetection.state}</span>
|
||||
</div>
|
||||
<Select
|
||||
type="single"
|
||||
@@ -197,62 +210,49 @@
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="rounded-xl border border-white/5 bg-white/5 px-3 py-2">
|
||||
<p class="text-[10px] uppercase tracking-wider text-gray-500">Detector State</p>
|
||||
<p class="mt-1 text-sm font-medium text-gray-200">{$appState.motionDetection.state}</p>
|
||||
<p class="text-[9px] font-bold uppercase tracking-widest text-gray-500">Detector</p>
|
||||
<p class="mt-0.5 text-xs font-bold text-gray-200">{$appState.motionDetection.state}</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-white/5 bg-white/5 px-3 py-2">
|
||||
<p class="text-[10px] uppercase tracking-wider text-gray-500">Motion Score</p>
|
||||
<p class="mt-1 text-sm font-medium text-gray-200">{$appState.motionDetection.score.toFixed(3)}</p>
|
||||
<p class="text-[9px] font-bold uppercase tracking-widest text-gray-500">Score</p>
|
||||
<p class="mt-0.5 text-xs font-bold text-gray-200">{$appState.motionDetection.score.toFixed(3)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-3 rounded-xl border border-white/5 bg-white/5 px-3 py-2">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-200">Show detector debug info</p>
|
||||
<p class="text-[11px] text-gray-500">Useful while tuning thresholds and sample profiles.</p>
|
||||
<div class="space-y-0.5">
|
||||
<p class="text-xs font-bold text-gray-200">Debug Overlay</p>
|
||||
<p class="text-[10px] text-gray-500">Sample profiles & metrics.</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={$appState.motionDetection.debug}
|
||||
aria-label="Toggle motion detector debug info"
|
||||
title="Toggle motion detector debug info"
|
||||
class="inline-flex h-7 w-12 items-center rounded-full border border-white/10 p-1 transition-colors {$appState.motionDetection.debug ? 'bg-blue-500/80' : 'bg-white/10'}"
|
||||
onclick={() => appController.setMotionDetectionDebug(!$appState.motionDetection.debug)}
|
||||
>
|
||||
<span
|
||||
class="h-5 w-5 rounded-full bg-white transition-transform {$appState.motionDetection.debug ? 'translate-x-5' : 'translate-x-0'}"
|
||||
></span>
|
||||
</button>
|
||||
<Switch
|
||||
checked={$appState.motionDetection.debug}
|
||||
onCheckedChange={(v) => appController.setMotionDetectionDebug(v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
|
||||
<div class="space-y-2">
|
||||
{#if !$appState.isMotionActive}
|
||||
<Button
|
||||
id="startMotionBtn"
|
||||
variant="outline"
|
||||
class="h-14 w-full justify-start rounded-xl border-white/5 bg-white/5 font-medium hover:border-red-500/30 hover:bg-red-500/10 hover:text-red-400"
|
||||
onclick={() => appController.startMotion()}
|
||||
>
|
||||
<svg data-icon="inline-start" xmlns="http://www.w3.org/2000/svg" class="mr-2 h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
Simulate Motion Event
|
||||
id="startMotionBtn"
|
||||
variant="outline"
|
||||
class="h-12 w-full justify-start rounded-xl border-white/5 bg-white/5 font-bold text-sm hover:border-blue-500/30 hover:bg-blue-500/10 hover:text-blue-400 group"
|
||||
onclick={() => appController.startMotion()}
|
||||
>
|
||||
<Zap class="mr-3 h-4 w-4 text-gray-400 group-hover:text-blue-400" />
|
||||
Simulate Motion
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
id="endMotionBtn"
|
||||
variant="outline"
|
||||
class="h-14 w-full justify-start rounded-xl border-white/5 bg-white/5 font-medium hover:bg-white/10 disabled:opacity-30"
|
||||
onclick={() => appController.endMotion()}
|
||||
>
|
||||
<svg data-icon="inline-start" xmlns="http://www.w3.org/2000/svg" class="mr-2 h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
||||
</svg>
|
||||
Stop Recording
|
||||
id="endMotionBtn"
|
||||
variant="outline"
|
||||
class="h-12 w-full justify-start rounded-xl border-white/5 bg-white/5 font-bold text-sm hover:bg-white/10"
|
||||
onclick={() => appController.endMotion()}
|
||||
>
|
||||
<Square class="mr-3 h-4 w-4 text-red-500 animate-pulse" />
|
||||
Stop Recording
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user