fix:history, loading
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
1e163e3e29
commit
70e71acf3b
|
|
@ -2,48 +2,47 @@
|
|||
|
||||
import { ArrowLeftIcon, ChevronLeft } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer"
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import Image from "next/image"
|
||||
import { useEffect, useState } from "react"
|
||||
import { socket } from "@/components/socket"
|
||||
import { usePlateStore } from "@/components/zustand/plate-history"
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
|
||||
interface historyType {
|
||||
img: string
|
||||
plate: string
|
||||
}
|
||||
const stream_dummy = [
|
||||
"/app/static/uploads/ds1.mp4",
|
||||
"/app/static/uploads/ds2.mp4",
|
||||
"/app/static/uploads/ds3.mp4",
|
||||
"/app/static/uploads/ds4.mp4",
|
||||
{ id: "asssd", url: "/app/static/uploads/ds1.mp4" },
|
||||
{ id: "asaggssd", url: "/app/static/uploads/ds2.mp4" },
|
||||
{ id: "asss112d", url: "/app/static/uploads/ds3.mp4" },
|
||||
{ id: "asgsasssd", url: "/app/static/uploads/ds4.mp4" },
|
||||
]
|
||||
|
||||
export default function Etle() {
|
||||
const router = useRouter()
|
||||
const [sessionId, setSessionId] = useState<string | null>(null)
|
||||
const [imageSrc, setImageSrc] = useState<string>("")
|
||||
const [plate, setPlate] = useState<string>("")
|
||||
const [confidence, setConfidence] = useState<number | null>(null)
|
||||
const [history, setHistory] = useState<historyType[]>([])
|
||||
const activeStream = usePlateStore((s) => s.activeStreamId)
|
||||
const setActiveStream = usePlateStore((s) => s.setActiveStream)
|
||||
const history = usePlateStore((s) => s.history)
|
||||
const addPlate = usePlateStore((s) => s.addPlate)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const currentStream = usePlateStore.getState().activeStreamId
|
||||
|
||||
const startDetection = (index: number) => {
|
||||
const stream = stream_dummy[index]
|
||||
setLoading(true)
|
||||
if (history[activeStream]) setPlate("")
|
||||
setConfidence(null)
|
||||
const stream = stream_dummy[index].url
|
||||
|
||||
socket.emit("stop_detection")
|
||||
socket.emit("start_detection", {
|
||||
stream_url: stream,
|
||||
})
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -53,15 +52,19 @@ export default function Etle() {
|
|||
|
||||
socket.on("connected", (data) => {
|
||||
setSessionId(data.session_id)
|
||||
const stream = stream_dummy.find((a) => a.id == currentStream)
|
||||
setActiveStream(currentStream)
|
||||
socket.emit("start_detection", {
|
||||
stream_url: "/app/static/uploads/ds1.mp4",
|
||||
stream_url: stream?.url ?? stream_dummy[0].url,
|
||||
})
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
socket.on("video_frame", (data) => {
|
||||
const thumb = `data:image/jpeg;base64,${data.frame}`
|
||||
setImageSrc(thumb)
|
||||
// setHistory((prev) => [thumb, ...prev].slice(0, 20))
|
||||
})
|
||||
|
||||
socket.on("plate_detected", (data) => {
|
||||
|
|
@ -69,24 +72,24 @@ export default function Etle() {
|
|||
setConfidence(data.confidence ?? null)
|
||||
|
||||
const thumb = `data:image/jpeg;base64,${data.frame_thumb}`
|
||||
setHistory((prev) => {
|
||||
// cek apakah plate sudah pernah ada
|
||||
const exists = prev.some((item) => item.plate === data.plate_text)
|
||||
|
||||
if (exists) return prev // skip kalau duplicate
|
||||
if (!currentStream) return
|
||||
|
||||
return [{ img: thumb, plate: data.plate_text }, ...prev]
|
||||
addPlate(currentStream, {
|
||||
imgThumb: thumb,
|
||||
plate: data.plate_text,
|
||||
confidence: data.confidence,
|
||||
})
|
||||
})
|
||||
|
||||
// socket.on("detection_stopped", (data) => {
|
||||
// console.log("Total:", data.total)
|
||||
// })
|
||||
|
||||
socket.on("error", (data) => {
|
||||
console.error(data.message)
|
||||
})
|
||||
|
||||
socket.on("detection_stopped", (data) => {
|
||||
console.log("Selesai. Total:", data.total)
|
||||
})
|
||||
|
||||
return () => {
|
||||
socket.emit("stop_detection")
|
||||
socket.removeAllListeners()
|
||||
|
|
@ -94,11 +97,16 @@ export default function Etle() {
|
|||
}
|
||||
}, [])
|
||||
|
||||
// const startDetection = () => {
|
||||
// socket.emit("start_detection", {
|
||||
// stream_url: "/app/static/uploads/ds1",
|
||||
// })
|
||||
// }
|
||||
const [hasMounted, setHasMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setHasMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!hasMounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex h-[8vh] flex-row items-center gap-10 bg-[#0057B3] px-8 py-5">
|
||||
|
|
@ -109,20 +117,33 @@ export default function Etle() {
|
|||
</div>
|
||||
<div className="flex h-[92vh] flex-col items-start gap-5 bg-gray-200 p-8 text-black">
|
||||
<p className="text-2xl font-semibold">TRAFFIC VIOLATIONS</p>
|
||||
{/* <Button onClick={startDetection}>START DETECTION</Button> */}
|
||||
<div className="flex gap-2">
|
||||
<p>now {currentStream}</p>
|
||||
{stream_dummy.map((stream, index) => (
|
||||
<Button key={index} onClick={() => startDetection(index)}>
|
||||
<Button
|
||||
key={stream.id}
|
||||
onClick={() => {
|
||||
setActiveStream(stream.id)
|
||||
startDetection(index)
|
||||
}}
|
||||
className={`cursor-pointer ${currentStream == stream.id ? "border-2 bg-gray-50 p-2" : ""}`}
|
||||
>
|
||||
DS{index + 1}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{imageSrc && (
|
||||
{loading ? (
|
||||
<div className="w-screen rounded-lg md:w-full xl:w-180">
|
||||
<Spinner className="h-50 w-50" />
|
||||
</div>
|
||||
) : (
|
||||
imageSrc && (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt="Realtime"
|
||||
className="w-screen rounded-lg md:w-full xl:w-180"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -146,44 +167,24 @@ export default function Etle() {
|
|||
<DrawerContent>
|
||||
<div className="no-scrollbar space-y-3 overflow-y-auto bg-[#CBCBCBCC] px-4 py-4 text-black">
|
||||
<div className="rounded-lg bg-white p-3 text-lg">
|
||||
Total Image: {history.length}
|
||||
Total Image: {history[activeStream]?.length}
|
||||
</div>{" "}
|
||||
{history.map((item, index) => (
|
||||
{history[currentStream]?.map((item, index) => (
|
||||
<div key={index} className="rounded-lg bg-white p-3 text-lg">
|
||||
<Image
|
||||
src={item.img}
|
||||
src={item.imgThumb}
|
||||
width={1280}
|
||||
height={960}
|
||||
alt={"image" + index}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
<p>{item.plate}</p>
|
||||
<p>Plate : {item.plate}</p>
|
||||
<p>Confidence: {item.confidence}</p>
|
||||
</div>
|
||||
))}
|
||||
{/* {history.map((img, index) => (
|
||||
<div key={index} className="rounded-lg bg-white p-3">
|
||||
<img src={img.img} className="rounded-lg" />
|
||||
</div>
|
||||
))} */}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{/* <Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button className="fixed top-1/2 right-0 h-20 w-6 -translate-y-1/2 rounded-l-md">
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<div className="grid flex-1 auto-rows-min gap-6 bg-[#CBCBCBCC] px-4 py-10 text-black">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="rounded-lg bg-white p-3 text-lg">
|
||||
Total Image: 10
|
||||
</div>{" "}
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,13 +55,14 @@ function DrawerContent({
|
|||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
title=""
|
||||
className={cn(
|
||||
"group/drawer-content fixed z-50 flex h-auto flex-col bg-background text-sm data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-xl data-[vaul-drawer-direction=bottom]:border-t data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:rounded-r-xl data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:rounded-l-xl data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-xl data-[vaul-drawer-direction=top]:border-b data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto mt-4 hidden h-1 w-[100px] shrink-0 rounded-full bg-muted group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
<div className="mx-auto mt-4 hidden h-1 w-25 shrink-0 rounded-full bg-muted group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import { LoaderIcon } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
export function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||
return (
|
||||
<LoaderIcon
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
className={cn("size-4 animate-spin", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import { create } from "zustand"
|
||||
import { persist } from "zustand/middleware"
|
||||
|
||||
type PlateItem = {
|
||||
imgThumb: string
|
||||
plate: string
|
||||
confidence?: number | null
|
||||
}
|
||||
|
||||
type PlateState = {
|
||||
activeStreamId: string | ""
|
||||
history: Record<string, PlateItem[]>
|
||||
|
||||
setActiveStream: (id: string) => void
|
||||
addPlate: (streamId: string, newData: PlateItem) => void
|
||||
clearHistory: (streamId: string) => void
|
||||
}
|
||||
|
||||
export const usePlateStore = create<PlateState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// active stream
|
||||
activeStreamId: "",
|
||||
|
||||
setActiveStream: (id: string) => set({ activeStreamId: id }),
|
||||
|
||||
// history per stream
|
||||
history: {},
|
||||
|
||||
addPlate: (streamId: string, newData: PlateItem) => {
|
||||
const state = get()
|
||||
const currentHistory = state.history[streamId] || []
|
||||
|
||||
const index = currentHistory.findIndex(
|
||||
(item) => item.plate === newData.plate
|
||||
)
|
||||
|
||||
let updatedHistory
|
||||
|
||||
// kalau belum ada → tambah
|
||||
if (index === -1) {
|
||||
updatedHistory = [newData, ...currentHistory]
|
||||
} else {
|
||||
const existing = currentHistory[index]
|
||||
|
||||
// kalau confidence lebih tinggi → replace
|
||||
if ((newData.confidence ?? 0) > (existing.confidence ?? 0)) {
|
||||
updatedHistory = [
|
||||
newData,
|
||||
...currentHistory.filter((_, i) => i !== index),
|
||||
]
|
||||
} else {
|
||||
updatedHistory = currentHistory
|
||||
}
|
||||
}
|
||||
|
||||
updatedHistory = updatedHistory
|
||||
|
||||
set({
|
||||
history: {
|
||||
...state.history,
|
||||
[streamId]: updatedHistory,
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
// optional: clear history per stream
|
||||
clearHistory: (streamId) => {
|
||||
const state = get()
|
||||
const newHistory = { ...state.history }
|
||||
delete newHistory[streamId]
|
||||
|
||||
set({ history: newHistory })
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "plate-storage", // key localStorage
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
@ -38,9 +38,9 @@
|
|||
"tailwindcss": "^4.2.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vaul": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {}
|
||||
"vaul": "^1.1.2",
|
||||
"zustand": "^5.0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
|
|
@ -10639,6 +10639,34 @@
|
|||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
|
||||
"integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"tailwindcss": "^4.2.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vaul": "^1.1.2"
|
||||
"vaul": "^1.1.2",
|
||||
"zustand": "^5.0.12"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue