From 4e155654153bfbb6b26cd47e7e61c222cb61f3c3 Mon Sep 17 00:00:00 2001 From: Anant Rai Date: Fri, 9 May 2025 10:56:52 -0700 Subject: [PATCH] added labeling ui --- packages/suite-base/src/hooks/useLogName.ts | 33 ++++ .../src/panels/LabelUnsafeEvents/index.tsx | 186 ++++++++++++++++++ packages/suite-base/src/panels/index.ts | 7 + 3 files changed, 226 insertions(+) create mode 100644 packages/suite-base/src/hooks/useLogName.ts create mode 100644 packages/suite-base/src/panels/LabelUnsafeEvents/index.tsx diff --git a/packages/suite-base/src/hooks/useLogName.ts b/packages/suite-base/src/hooks/useLogName.ts new file mode 100644 index 0000000..74337d9 --- /dev/null +++ b/packages/suite-base/src/hooks/useLogName.ts @@ -0,0 +1,33 @@ +import { useState, useEffect } from "react"; +import { windowAppURLState } from "@lichtblick/suite-base/util/appURLState"; + +// Matches strings like "ailog_FFCAFF00-0998-410B-BF84-B96917AB1B01_2022_03_03-12_05_38" +const LOG_NAME_REGEX = /ailog_([A-Z0-9-]+)_(\d{4}_\d{2}_\d{2}-\d{2}_\d{2}_\d{2})/; + +/** + * Hook to derive the current log_name from the URL's dsParams.url. + * Updates on initial mount and whenever the URL changes (popstate). + * Returns the matched log_name, or an empty string if none. + */ +export function useLogName(): string { + const deriveLogName = (): string => { + const dsUrl = windowAppURLState()?.dsParams?.url; + const match = dsUrl?.match(LOG_NAME_REGEX); + return match?.[0] ?? ""; + }; + + const [logName, setLogName] = useState(() => deriveLogName()); + + useEffect(() => { + const handleUrlChange = () => { + setLogName(deriveLogName()); + }; + window.addEventListener("popstate", handleUrlChange); + // Also listen for pushState/replaceState if needed (optional) + return () => { + window.removeEventListener("popstate", handleUrlChange); + }; + }, []); + + return logName; +} diff --git a/packages/suite-base/src/panels/LabelUnsafeEvents/index.tsx b/packages/suite-base/src/panels/LabelUnsafeEvents/index.tsx new file mode 100644 index 0000000..da27821 --- /dev/null +++ b/packages/suite-base/src/panels/LabelUnsafeEvents/index.tsx @@ -0,0 +1,186 @@ +import { Box, Button, TextField, Typography, Stack, Paper } from "@mui/material"; +import { ReactElement, useState } from "react"; +import Panel from "../../components/Panel"; +import { PanelConfig, SaveConfig } from "@lichtblick/suite-base/types/panels"; +import { useMessagePipeline, useMessagePipelineGetter } from "@lichtblick/suite-base/components/MessagePipeline"; +import { Time, fromSec, add as addTimes } from "@lichtblick/rostime"; + +// Define the Segment interface +interface Segment { + id: string; + caption: string; + startTime: number; + endTime: number; +} + +// Mock data for initial segments +const mockSegments: Segment[] = [ + { id: "1", caption: "Person crossing street", startTime: 10, endTime: 15 }, + { id: "2", caption: "Car turning without signal", startTime: 25, endTime: 30 }, + { id: "3", caption: "Near collision at intersection", startTime: 45, endTime: 52 }, +]; + +function LabelUnsafeEventsPanelInner(): ReactElement { + // Get the messagePipeline getter function to access latest state in callbacks + const messagePipeline = useMessagePipelineGetter(); + + // State for the segments + const [segments, setSegments] = useState(mockSegments); + const [unsavedChanges, setUnsavedChanges] = useState(false); + + // Handler for updating a segment's caption + const handleCaptionChange = (id: string, newCaption: string) => { + setSegments(prevSegments => + prevSegments.map(segment => + segment.id === id ? { ...segment, caption: newCaption } : segment + ) + ); + setUnsavedChanges(true); + }; + + // Handler for updating a segment's start time + const handleStartTimeChange = (id: string, newStartTime: string) => { + const timeValue = parseFloat(newStartTime); + if (!isNaN(timeValue)) { + setSegments(prevSegments => + prevSegments.map(segment => + segment.id === id ? { ...segment, startTime: timeValue } : segment + ) + ); + setUnsavedChanges(true); + } + }; + + // Handler for updating a segment's end time + const handleEndTimeChange = (id: string, newEndTime: string) => { + const timeValue = parseFloat(newEndTime); + if (!isNaN(timeValue)) { + setSegments(prevSegments => + prevSegments.map(segment => + segment.id === id ? { ...segment, endTime: timeValue } : segment + ) + ); + setUnsavedChanges(true); + } + }; + + // Handler for seek button + const handleSeek = (seekSeconds: number) => { + // Get the latest state including seekPlayback function and startTime + const { + seekPlayback, + playerState: { activeData: { startTime } = {} }, + } = messagePipeline(); + + // Return early if seekPlayback or startTime is not available + if (!seekPlayback || !startTime) { + console.warn("Seek functionality not available - missing seekPlayback or startTime"); + return; + } + + console.log(`Seeking to ${seekSeconds} seconds from startTime:`, startTime); + + // Calculate the absolute time by adding the seek seconds to the startTime + const seekTime = addTimes(startTime, fromSec(seekSeconds)); + + // Perform the seek + seekPlayback(seekTime); + }; + + // Handler for save button + const handleSave = () => { + // This will be implemented later to save segments to storage/backend + console.log("Saving segments:", segments); + setUnsavedChanges(false); + }; + + return ( + + + Unsafe Events + + + + {segments.map((segment) => ( + + + handleCaptionChange(segment.id, e.target.value)} + variant="outlined" + size="small" + /> + + + handleStartTimeChange(segment.id, e.target.value)} + variant="outlined" + size="small" + inputProps={{ step: 0.1 }} + sx={{ width: 120 }} + /> + + handleEndTimeChange(segment.id, e.target.value)} + variant="outlined" + size="small" + inputProps={{ step: 0.1 }} + sx={{ width: 120 }} + /> + + + + + + ))} + + + + + + + ); +} + +interface LabelUnsafeEventsConfig extends PanelConfig { + // You can add panel-specific config options here + segments?: Segment[]; +} + +const LabelUnsafeEventsPanel = ({ + config, + saveConfig, +}: { + config: LabelUnsafeEventsConfig; + saveConfig: SaveConfig; +}) => { + return ; +}; + +LabelUnsafeEventsPanel.panelType = "LabelUnsafeEvents"; +LabelUnsafeEventsPanel.defaultConfig = { + segments: mockSegments, +}; + +export default Panel(LabelUnsafeEventsPanel); diff --git a/packages/suite-base/src/panels/index.ts b/packages/suite-base/src/panels/index.ts index 57e22d4..f37a5e5 100644 --- a/packages/suite-base/src/panels/index.ts +++ b/packages/suite-base/src/panels/index.ts @@ -197,4 +197,11 @@ export const getBuiltin: (t: TFunction<"panels">) => PanelInfo[] = (t) => [ thumbnail: videoPlayerClientThumbnail, module: async () => await import("./VideoPlayerClient"), }, + { + title: "labelUnsafeEvents", + type: "LabelUnsafeEvents", + description: "Used for labeling unsafe events in a log.", + thumbnail: dataSourceInfoThumbnail, + module: async () => await import("./LabelUnsafeEvents"), + }, ];