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 { ArrowLeftIcon, ChevronLeft } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useRouter } from "next/navigation"
|
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"
|
||||||
import {
|
|
||||||
Drawer,
|
|
||||||
DrawerClose,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerDescription,
|
|
||||||
DrawerFooter,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerTrigger,
|
|
||||||
} from "@/components/ui/drawer"
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { socket } from "@/components/socket"
|
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 = [
|
const stream_dummy = [
|
||||||
"/app/static/uploads/ds1.mp4",
|
{ id: "asssd", url: "/app/static/uploads/ds1.mp4" },
|
||||||
"/app/static/uploads/ds2.mp4",
|
{ id: "asaggssd", url: "/app/static/uploads/ds2.mp4" },
|
||||||
"/app/static/uploads/ds3.mp4",
|
{ id: "asss112d", url: "/app/static/uploads/ds3.mp4" },
|
||||||
"/app/static/uploads/ds4.mp4",
|
{ id: "asgsasssd", url: "/app/static/uploads/ds4.mp4" },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function Etle() {
|
export default function Etle() {
|
||||||
const router = useRouter()
|
|
||||||
const [sessionId, setSessionId] = useState<string | null>(null)
|
const [sessionId, setSessionId] = useState<string | null>(null)
|
||||||
const [imageSrc, setImageSrc] = useState<string>("")
|
const [imageSrc, setImageSrc] = useState<string>("")
|
||||||
const [plate, setPlate] = useState<string>("")
|
const [plate, setPlate] = useState<string>("")
|
||||||
const [confidence, setConfidence] = useState<number | null>(null)
|
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 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("stop_detection")
|
||||||
socket.emit("start_detection", {
|
socket.emit("start_detection", {
|
||||||
stream_url: stream,
|
stream_url: stream,
|
||||||
})
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false)
|
||||||
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -53,15 +52,19 @@ export default function Etle() {
|
||||||
|
|
||||||
socket.on("connected", (data) => {
|
socket.on("connected", (data) => {
|
||||||
setSessionId(data.session_id)
|
setSessionId(data.session_id)
|
||||||
|
const stream = stream_dummy.find((a) => a.id == currentStream)
|
||||||
|
setActiveStream(currentStream)
|
||||||
socket.emit("start_detection", {
|
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) => {
|
socket.on("video_frame", (data) => {
|
||||||
const thumb = `data:image/jpeg;base64,${data.frame}`
|
const thumb = `data:image/jpeg;base64,${data.frame}`
|
||||||
setImageSrc(thumb)
|
setImageSrc(thumb)
|
||||||
// setHistory((prev) => [thumb, ...prev].slice(0, 20))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on("plate_detected", (data) => {
|
socket.on("plate_detected", (data) => {
|
||||||
|
|
@ -69,24 +72,24 @@ export default function Etle() {
|
||||||
setConfidence(data.confidence ?? null)
|
setConfidence(data.confidence ?? null)
|
||||||
|
|
||||||
const thumb = `data:image/jpeg;base64,${data.frame_thumb}`
|
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) => {
|
socket.on("error", (data) => {
|
||||||
console.error(data.message)
|
console.error(data.message)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
socket.on("detection_stopped", (data) => {
|
||||||
|
console.log("Selesai. Total:", data.total)
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.emit("stop_detection")
|
socket.emit("stop_detection")
|
||||||
socket.removeAllListeners()
|
socket.removeAllListeners()
|
||||||
|
|
@ -94,11 +97,16 @@ export default function Etle() {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// const startDetection = () => {
|
const [hasMounted, setHasMounted] = useState(false)
|
||||||
// socket.emit("start_detection", {
|
|
||||||
// stream_url: "/app/static/uploads/ds1",
|
useEffect(() => {
|
||||||
// })
|
setHasMounted(true)
|
||||||
// }
|
}, [])
|
||||||
|
|
||||||
|
if (!hasMounted) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex h-[8vh] flex-row items-center gap-10 bg-[#0057B3] px-8 py-5">
|
<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>
|
||||||
<div className="flex h-[92vh] flex-col items-start gap-5 bg-gray-200 p-8 text-black">
|
<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>
|
<p className="text-2xl font-semibold">TRAFFIC VIOLATIONS</p>
|
||||||
{/* <Button onClick={startDetection}>START DETECTION</Button> */}
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<p>now {currentStream}</p>
|
||||||
{stream_dummy.map((stream, index) => (
|
{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}
|
DS{index + 1}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{imageSrc && (
|
{loading ? (
|
||||||
<img
|
<div className="w-screen rounded-lg md:w-full xl:w-180">
|
||||||
src={imageSrc}
|
<Spinner className="h-50 w-50" />
|
||||||
alt="Realtime"
|
</div>
|
||||||
className="w-screen rounded-lg md:w-full xl:w-180"
|
) : (
|
||||||
/>
|
imageSrc && (
|
||||||
|
<img
|
||||||
|
src={imageSrc}
|
||||||
|
alt="Realtime"
|
||||||
|
className="w-screen rounded-lg md:w-full xl:w-180"
|
||||||
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -146,44 +167,24 @@ export default function Etle() {
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<div className="no-scrollbar space-y-3 overflow-y-auto bg-[#CBCBCBCC] px-4 py-4 text-black">
|
<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">
|
<div className="rounded-lg bg-white p-3 text-lg">
|
||||||
Total Image: {history.length}
|
Total Image: {history[activeStream]?.length}
|
||||||
</div>{" "}
|
</div>{" "}
|
||||||
{history.map((item, index) => (
|
{history[currentStream]?.map((item, index) => (
|
||||||
<div key={index} className="rounded-lg bg-white p-3 text-lg">
|
<div key={index} className="rounded-lg bg-white p-3 text-lg">
|
||||||
<Image
|
<Image
|
||||||
src={item.img}
|
src={item.imgThumb}
|
||||||
width={1280}
|
width={1280}
|
||||||
height={960}
|
height={960}
|
||||||
alt={"image" + index}
|
alt={"image" + index}
|
||||||
className="rounded-lg"
|
className="rounded-lg"
|
||||||
/>
|
/>
|
||||||
<p>{item.plate}</p>
|
<p>Plate : {item.plate}</p>
|
||||||
|
<p>Confidence: {item.confidence}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{/* {history.map((img, index) => (
|
|
||||||
<div key={index} className="rounded-lg bg-white p-3">
|
|
||||||
<img src={img.img} className="rounded-lg" />
|
|
||||||
</div>
|
|
||||||
))} */}
|
|
||||||
</div>
|
</div>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,14 @@ function DrawerContent({
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerPrimitive.Content
|
<DrawerPrimitive.Content
|
||||||
data-slot="drawer-content"
|
data-slot="drawer-content"
|
||||||
|
title=""
|
||||||
className={cn(
|
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",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...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}
|
{children}
|
||||||
</DrawerPrimitive.Content>
|
</DrawerPrimitive.Content>
|
||||||
</DrawerPortal>
|
</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",
|
"tailwindcss": "^4.2.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vaul": "^1.1.2"
|
"vaul": "^1.1.2",
|
||||||
},
|
"zustand": "^5.0.12"
|
||||||
"devDependencies": {}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
|
|
@ -10639,6 +10639,34 @@
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
"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",
|
"tailwindcss": "^4.2.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vaul": "^1.1.2"
|
"vaul": "^1.1.2",
|
||||||
|
"zustand": "^5.0.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue