mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-13 06:35:24 +03:00
Add support for review information side panel
This commit is contained in:
parent
8b8d6f8e7e
commit
65ee873cf8
@ -251,6 +251,26 @@ def events():
|
||||
return jsonify(list(events))
|
||||
|
||||
|
||||
@EventBp.route("/event_ids")
|
||||
def event_ids():
|
||||
idString = request.args.get("ids")
|
||||
ids = idString.split(",")
|
||||
|
||||
if not ids:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Valid list of ids must be sent"}),
|
||||
400,
|
||||
)
|
||||
|
||||
try:
|
||||
events = Event.select().where(Event.id << ids).dicts().iterator()
|
||||
return jsonify(list(events))
|
||||
except Exception:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Events not found"}), 400
|
||||
)
|
||||
|
||||
|
||||
@EventBp.route("/events/search")
|
||||
def events_search():
|
||||
query = request.args.get("query", type=str)
|
||||
|
||||
@ -7,6 +7,8 @@ import { useFormattedTimestamp } from "@/hooks/use-date-utils";
|
||||
import { getIconForLabel } from "@/utils/iconUtil";
|
||||
import { useApiHost } from "@/api";
|
||||
import { ReviewSegment } from "@/types/review";
|
||||
import { Event } from "@/types/event";
|
||||
import { useMemo } from "react";
|
||||
|
||||
type ReviewDetailDialogProps = {
|
||||
review?: ReviewSegment;
|
||||
@ -24,6 +26,18 @@ export default function ReviewDetailDialog({
|
||||
|
||||
// data
|
||||
|
||||
const { data: events } = useSWR<Event[]>(
|
||||
review ? ["event_ids", { ids: review.data.detections.join(",") }] : null,
|
||||
);
|
||||
|
||||
const hasMismatch = useMemo(() => {
|
||||
if (!review || !events) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return events.length != review?.data.detections.length;
|
||||
}, [review, events]);
|
||||
|
||||
const formattedDate = useFormattedTimestamp(
|
||||
review?.start_time ?? 0,
|
||||
config?.ui.time_format == "24hour"
|
||||
@ -54,34 +68,6 @@ export default function ReviewDetailDialog({
|
||||
<div className="mt-3 flex size-full flex-col gap-5 md:mt-0">
|
||||
<div className="flex w-full flex-row">
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm text-primary/40">Labels</div>
|
||||
<div className="flex flex-col items-start gap-2 text-sm capitalize">
|
||||
{[
|
||||
...new Set([
|
||||
...(review.data.objects || []),
|
||||
...(review.data.sub_labels || []),
|
||||
...(review.data.audio || []),
|
||||
]),
|
||||
]
|
||||
.filter(
|
||||
(item) =>
|
||||
item !== undefined && !item.includes("-verified"),
|
||||
)
|
||||
.sort()
|
||||
.map((obj) => {
|
||||
return (
|
||||
<div
|
||||
key={obj}
|
||||
className="flex flex-row items-center gap-2 text-sm capitalize"
|
||||
>
|
||||
{getIconForLabel(obj, "size-3 text-white")}
|
||||
{obj}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm text-primary/40">Camera</div>
|
||||
<div className="text-sm capitalize">
|
||||
@ -94,26 +80,70 @@ export default function ReviewDetailDialog({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-2 px-6">
|
||||
{review.data.detections.map((eventId) => {
|
||||
return (
|
||||
<img
|
||||
key={eventId}
|
||||
className="aspect-video select-none rounded-lg object-contain transition-opacity"
|
||||
style={
|
||||
isIOS
|
||||
? {
|
||||
WebkitUserSelect: "none",
|
||||
WebkitTouchCallout: "none",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
draggable={false}
|
||||
src={`${apiHost}api/events/${eventId}/thumbnail.jpg`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm text-primary/40">Objects</div>
|
||||
<div className="flex flex-col items-start gap-2 text-sm capitalize">
|
||||
{events?.map((event) => {
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className="flex flex-row items-center gap-2 text-sm capitalize"
|
||||
>
|
||||
{getIconForLabel(event.label, "size-3 text-white")}
|
||||
{event.sub_label ?? event.label} (
|
||||
{Math.round(event.data.top_score * 100)}%)
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm text-primary/40">Zones</div>
|
||||
<div className="flex flex-col items-start gap-2 text-sm capitalize">
|
||||
{review.data.zones.map((zone) => {
|
||||
return (
|
||||
<div
|
||||
key={zone}
|
||||
className="flex flex-row items-center gap-2 text-sm capitalize"
|
||||
>
|
||||
{zone.replaceAll("_", " ")}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{hasMismatch && (
|
||||
<div className="p-4 text-center text-sm">
|
||||
Some objects that were detected are not included in this list
|
||||
because the object does not have a snapshot
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full flex-col gap-2 px-6">
|
||||
{events?.map((event) => {
|
||||
return (
|
||||
<img
|
||||
key={event.id}
|
||||
className="aspect-video select-none rounded-lg object-contain transition-opacity"
|
||||
style={
|
||||
isIOS
|
||||
? {
|
||||
WebkitUserSelect: "none",
|
||||
WebkitTouchCallout: "none",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
draggable={false}
|
||||
src={
|
||||
event.has_snapshot
|
||||
? `${apiHost}api/events/${event.id}/thumbnail.jpg`
|
||||
: `${apiHost}api/events/${event.id}/thumbnail.jpg`
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Content>
|
||||
|
||||
@ -4,7 +4,7 @@ import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import monacoEditorPlugin from "vite-plugin-monaco-editor";
|
||||
|
||||
const proxyHost = process.env.PROXY_HOST || "192.168.50.106:5002";
|
||||
const proxyHost = process.env.PROXY_HOST || "localhost:5000";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user