913 lines
36 KiB
JavaScript
913 lines
36 KiB
JavaScript
"use client";
|
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
import Login from "./components/Login";
|
|
import EmbedTimeline from "./components/EmbedTimeline";
|
|
import VideoPlayer from "./components/VideoPlayer";
|
|
import CustomDateRangePicker from "./components/DateRangePicker";
|
|
import "./App.css";
|
|
import StatusesDisplayHUD from "./components/StatusDisplay";
|
|
|
|
function App() {
|
|
const original_data = useRef(null);
|
|
const chartRef = useRef(null);
|
|
const [dataResults, setDataResults] = useState(null);
|
|
const [statusMessages, setStatusMessages] = useState([]);
|
|
const [markerTime, setMarkerTime] = useState(0);
|
|
const playerRef = useRef(null);
|
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
const playerInstanceRef = useRef(null);
|
|
const [selectedCamera, setSelectedCamera] = useState("Leopards 1");
|
|
const [selectedHighRes, setSelectedHighRes] = useState(false);
|
|
const [videoPlaying, setVideoPlaying] = useState(true);
|
|
// State for the values
|
|
window.chartRef = chartRef;
|
|
window.playerRef = playerRef;
|
|
window.playerInstanceRef = playerInstanceRef;
|
|
// Slider states
|
|
|
|
const inputRef = useRef(null);
|
|
|
|
const [sliderMin, setSliderMin] = useState(0.0);
|
|
const [sliderMax, setSliderMax] = useState(1.0);
|
|
// Date range states
|
|
//
|
|
|
|
const [startRange, setStartRange] = useState(
|
|
new Date(new Date().getTime() - 1 * 24 * 60 * 60 * 1000),
|
|
);
|
|
const [endRange, setEndRange] = useState(new Date());
|
|
// const [endRange, setEndRange] = useState(new Date(new Date().getTime() - 6 * 24 * 60 * 60 * 1000));
|
|
const [queryText, setQueryText] = useState("Two clouded leopards being aggressive");
|
|
const [sliderValue, setSliderValue] = useState(0);
|
|
|
|
// State to track last submitted values
|
|
const [lastSubmitted, setLastSubmitted] = useState({
|
|
startRange,
|
|
endRange,
|
|
queryText,
|
|
selectedCamera,
|
|
});
|
|
|
|
// // Check if any value has changed
|
|
// const queryChanged =
|
|
// startRange !== lastSubmitted.startRange ||
|
|
// endRange !== lastSubmitted.endRange ||
|
|
// queryText !== lastSubmitted.queryText;
|
|
|
|
// Check if queryText is different from lastSubmitted.queryText
|
|
const textChanged = queryText !== lastSubmitted.queryText;
|
|
|
|
const startChanged =
|
|
startRange.getTime() !== new Date(lastSubmitted.startRange).getTime();
|
|
|
|
const endChanged =
|
|
endRange.getTime() !== new Date(lastSubmitted.endRange).getTime();
|
|
|
|
const cameraChanged = selectedCamera !== lastSubmitted.selectedCamera;
|
|
|
|
const queryChanged =
|
|
textChanged || startChanged || endChanged || cameraChanged;
|
|
|
|
const streamComputeStatus = () => {
|
|
fetch("api/return_status")
|
|
.then((response) => {
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = ""; // Accumulate partial text
|
|
|
|
function read() {
|
|
reader.read().then(({ done, value }) => {
|
|
if (done) {
|
|
if (buffer) {
|
|
}
|
|
setStatusMessages([]);
|
|
|
|
return;
|
|
}
|
|
// Decode only the new chunk
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
// If your server sends lines, split and log only complete lines:
|
|
let lines = buffer.split("\n");
|
|
buffer = lines.pop(); // Save incomplete line for next chunk
|
|
|
|
for (const line of lines) {
|
|
if (line.trim()) {
|
|
let c_line = JSON.parse(line);
|
|
|
|
if (c_line["task"] !== "DONE_QUIT") {
|
|
setStatusMessages((msgs) => [
|
|
...msgs,
|
|
c_line,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
read();
|
|
});
|
|
}
|
|
read();
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error while streaming status:", error);
|
|
});
|
|
};
|
|
// Function to resubmit fetch
|
|
const handleResubmit = (doTestMode = false) => {
|
|
setVideoPlaying(false);
|
|
let startRangeUse;
|
|
let endRangeUse;
|
|
setDrawerOpen(false);
|
|
|
|
if (doTestMode == true) {
|
|
startRangeUse = new Date(
|
|
new Date().getTime() - 2 * 24 * 60 * 60 * 1000,
|
|
);
|
|
endRangeUse = new Date(
|
|
new Date().getTime() - 1 * 24 * 60 * 60 * 1000,
|
|
);
|
|
} else {
|
|
startRangeUse = startRange;
|
|
endRangeUse = endRange;
|
|
}
|
|
|
|
setStartRange(startRangeUse);
|
|
setEndRange(endRangeUse);
|
|
|
|
const params = new URLSearchParams();
|
|
params.append("startRange", startRangeUse.toISOString());
|
|
params.append("endRange", endRangeUse.toISOString());
|
|
params.append("threshold", 0.0);
|
|
params.append("query", queryText);
|
|
params.append("camera", selectedCamera);
|
|
setDataResults({ videos: [], breaks: [] });
|
|
|
|
authenticatedFetch("api/videos.json?" + params.toString())
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
streamComputeStatus();
|
|
// Don't setDataResults here, since it's just {"status": ...}
|
|
pollForResult(); // Start polling for the real result
|
|
});
|
|
|
|
setLastSubmitted({
|
|
startRange: startRangeUse,
|
|
endRange: endRangeUse,
|
|
queryText,
|
|
selectedCamera,
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
function selectHighResFunc(selectedHighRes, setSelectedHighRes, toggleCheckbox = true) {
|
|
console.log(selectedHighRes);
|
|
const params = new URLSearchParams();
|
|
params.append("do_high_res", !selectedHighRes);
|
|
authenticatedFetch("api/set_parameter?" + params.toString())
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
if (toggleCheckbox) {
|
|
setSelectedHighRes(!selectedHighRes);
|
|
}
|
|
});
|
|
}
|
|
function updateDataAndValue(newValue) {
|
|
const floatValue = parseFloat(newValue);
|
|
setSliderValue(floatValue);
|
|
var newData = JSON.parse(JSON.stringify(original_data.current));
|
|
newData["videos"] = newData["videos"].filter(
|
|
(vid) => vid["embed_scores"]["score"][1] >= floatValue,
|
|
);
|
|
newData["threshold"] = floatValue;
|
|
setDataResults(newData);
|
|
}
|
|
|
|
function setMarkerValueNonReactive(inputValue) {
|
|
let chart = chartRef.current.getEchartsInstance();
|
|
let options = chart.getOption();
|
|
let mappers = options["mappers"];
|
|
|
|
let vv = {
|
|
xAxis: mappers["real_to_virtual"](new Date(inputValue)),
|
|
lineStyle: { type: "solid", color: "#FF0000", width: 2 },
|
|
label: {
|
|
show: false,
|
|
formatter: "Break",
|
|
position: "bottom",
|
|
color: "#888",
|
|
fontSize: 10,
|
|
},
|
|
};
|
|
|
|
let markLine = {
|
|
symbol: ["none", "none"],
|
|
data: [vv],
|
|
lineStyle: { type: "dashed", color: "#FF0000", width: 2 },
|
|
silent: true,
|
|
animation: false,
|
|
};
|
|
|
|
// if ("markLine" in options["series"][1]) {
|
|
if (false) {
|
|
let vv_new = {
|
|
xAxis: mappers["real_to_virtual"](new Date(inputValue)),
|
|
};
|
|
let markLine_new = {
|
|
data: [vv_new],
|
|
};
|
|
|
|
chart.setOption(
|
|
{
|
|
series: [{}, { markLine: { data: [vv_new] } }],
|
|
},
|
|
false,
|
|
["series.markLine"],
|
|
);
|
|
} else {
|
|
chart.setOption(
|
|
{
|
|
series: [{}, { markLine: markLine }],
|
|
},
|
|
false,
|
|
["series.markLine"],
|
|
);
|
|
}
|
|
}
|
|
const handleTimelineClick = useCallback((path, timeoffset) => {
|
|
if (playerRef.current && playerInstanceRef.current) {
|
|
const player = playerInstanceRef.current;
|
|
const token = localStorage.getItem("access_token");
|
|
|
|
const authenticatedUrl = `api/${path}?token=${token}`;
|
|
player.reset();
|
|
|
|
// Configure player for range request support
|
|
const videoElement = player.el().querySelector("video");
|
|
if (videoElement) {
|
|
videoElement.crossOrigin = "anonymous";
|
|
videoElement.preload = "metadata";
|
|
}
|
|
|
|
player.src({
|
|
src: authenticatedUrl,
|
|
type: "video/mp4",
|
|
withCredentials: false,
|
|
});
|
|
setVideoPlaying(true);
|
|
// Ensure range headers are sent for seeking
|
|
player.ready(() => {
|
|
const tech = player.tech();
|
|
if (tech && tech.el_) {
|
|
tech.el_.addEventListener("loadstart", () => {
|
|
// Force range request capability
|
|
if (
|
|
tech.el_.seekable &&
|
|
tech.el_.seekable.length === 0
|
|
) {
|
|
console.log(
|
|
"Video doesn't support seeking - range headers may be needed",
|
|
);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
player.load();
|
|
|
|
// Add multiple event listeners for debugging
|
|
player.one("loadedmetadata", () => {
|
|
console.log("Video metadata loaded");
|
|
console.log("Video duration:", player.duration());
|
|
console.log("Time offset:", timeoffset);
|
|
});
|
|
|
|
// Wait for the video to be ready for seeking
|
|
player.one("canplay", () => {
|
|
console.log("Video can start playing - setting time");
|
|
|
|
const duration = player.duration();
|
|
|
|
// Ensure timeoffset is valid
|
|
const seekTime = Math.max(
|
|
0,
|
|
Math.min(timeoffset, duration - 0.1),
|
|
);
|
|
|
|
console.log("Seeking to:", seekTime, "of", duration);
|
|
player.currentTime(seekTime);
|
|
|
|
// Wait a bit before playing to ensure seek completed
|
|
setTimeout(() => {
|
|
const playPromise = player.play();
|
|
if (playPromise !== undefined) {
|
|
playPromise
|
|
.then(() => {
|
|
console.log(
|
|
"Video started playing at time:",
|
|
player.currentTime(),
|
|
);
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error playing video:", error);
|
|
});
|
|
}
|
|
}, 100);
|
|
});
|
|
|
|
player.one("error", (e) => {
|
|
console.error("Video error:", e);
|
|
console.error("Player error:", player.error());
|
|
});
|
|
|
|
player.one("loadstart", () => {
|
|
console.log("Load started");
|
|
});
|
|
|
|
player.one("canplay", () => {
|
|
console.log("Video can start playing");
|
|
});
|
|
} else {
|
|
console.error("Player ref not available");
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
selectHighResFunc(
|
|
selectedHighRes,
|
|
setSelectedHighRes,
|
|
false
|
|
)
|
|
handleResubmit(params.get("test_mode") === "true");
|
|
}, []);
|
|
|
|
// useEffect(() => {
|
|
// if (startRange && endRange) {
|
|
// handleResubmit();
|
|
// }
|
|
// }, [startRange, endRange]);
|
|
|
|
function pollForResult() {
|
|
const poll = () => {
|
|
authenticatedFetch("api/videos_result.json")
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
if (data.status === "processing") {
|
|
setTimeout(poll, 250); // Try again in 1 second
|
|
} else {
|
|
const max_value = Math.max(
|
|
...data["videos"].map(
|
|
(vid) => vid["embed_scores"]["score"][1],
|
|
),
|
|
);
|
|
setSliderMax(max_value);
|
|
|
|
original_data.current = data;
|
|
setDataResults(data);
|
|
updateDataAndValue(sliderValue)
|
|
}
|
|
});
|
|
};
|
|
poll();
|
|
}
|
|
|
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
const [checkingAuth, setCheckingAuth] = useState(true);
|
|
|
|
// Check authentication on load
|
|
useEffect(() => {
|
|
const token = localStorage.getItem("access_token");
|
|
if (token) {
|
|
// Verify token is still valid
|
|
fetch("/api/videos_result.json", {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
})
|
|
.then((response) => {
|
|
if (response.ok) {
|
|
setIsAuthenticated(true);
|
|
} else {
|
|
localStorage.removeItem("access_token");
|
|
setIsAuthenticated(false);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
localStorage.removeItem("access_token");
|
|
setIsAuthenticated(false);
|
|
})
|
|
.finally(() => {
|
|
setCheckingAuth(false);
|
|
});
|
|
} else {
|
|
setCheckingAuth(false);
|
|
}
|
|
}, []);
|
|
|
|
// Add authentication header to all API calls
|
|
const authenticatedFetch = useCallback((url, options = {}) => {
|
|
const token = localStorage.getItem("access_token");
|
|
const headers = {
|
|
...options.headers,
|
|
Authorization: `Bearer ${token}`,
|
|
};
|
|
return fetch(url, { ...options, headers });
|
|
}, []);
|
|
|
|
const handleLoginSuccess = () => {
|
|
setIsAuthenticated(true);
|
|
handleResubmit(params.get("test_mode") === "true");
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
localStorage.removeItem("access_token");
|
|
setIsAuthenticated(false);
|
|
};
|
|
|
|
if (checkingAuth) {
|
|
return (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
height: "100vh",
|
|
background: "#181a20",
|
|
color: "#fff",
|
|
}}
|
|
>
|
|
Loading...
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!isAuthenticated) {
|
|
return <Login onLoginSuccess={handleLoginSuccess} />;
|
|
}
|
|
|
|
return (
|
|
<div className="app-container">
|
|
{/* {drawerOpen && (
|
|
<div
|
|
className="drawer-backdrop"
|
|
onClick={() => setDrawerOpen(false)}
|
|
style={{
|
|
position: "fixed",
|
|
top: 0,
|
|
left: 0,
|
|
width: "100vw",
|
|
height: "100vh",
|
|
background: "rgba(0, 0, 0, 0.5)",
|
|
zIndex: 1400,
|
|
opacity: drawerOpen ? 1 : 0,
|
|
transition: "opacity 0.3s ease",
|
|
}}
|
|
/>
|
|
)} */}
|
|
|
|
{drawerOpen && (
|
|
<div
|
|
className={`controls-section ${
|
|
drawerOpen ? "drawer-open" : ""
|
|
}`}
|
|
>
|
|
<div
|
|
className="control-group"
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "20px",
|
|
justifyContent: "flex-start",
|
|
}}
|
|
>
|
|
<button
|
|
onClick={handleLogout}
|
|
style={{
|
|
flexShrink: 0,
|
|
width: "auto",
|
|
minWidth: "80px",
|
|
marginLeft: "20px",
|
|
}}
|
|
>
|
|
Logout
|
|
</button>
|
|
<div
|
|
className="radio-group"
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "8px",
|
|
flexShrink: 0,
|
|
width: "auto",
|
|
border: "2px solid #4a5568",
|
|
borderRadius: "8px",
|
|
padding: "12px",
|
|
backgroundColor: "#2d3748",
|
|
boxShadow: "0 2px 4px rgba(0,0,0,0.3)",
|
|
}}
|
|
>
|
|
<label
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "6px",
|
|
color: "white",
|
|
justifyContent: "flex-start",
|
|
cursor: "pointer",
|
|
}}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="camera_choice"
|
|
value="Leopards 1"
|
|
checked={selectedCamera === "Leopards 1"}
|
|
onChange={(e) =>
|
|
setSelectedCamera(e.target.value)
|
|
}
|
|
/>
|
|
Leopards 1
|
|
</label>
|
|
<label
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "6px",
|
|
color: "white",
|
|
justifyContent: "flex-start",
|
|
cursor: "pointer",
|
|
}}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="camera_choice"
|
|
value="Leopards 2"
|
|
checked={selectedCamera === "Leopards 2"}
|
|
onChange={(e) =>
|
|
setSelectedCamera(e.target.value)
|
|
}
|
|
/>
|
|
Leopards 2
|
|
</label>
|
|
<label
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "6px",
|
|
color: "white",
|
|
justifyContent: "flex-start",
|
|
cursor: "pointer",
|
|
}}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="camera_choice"
|
|
value="Leopards 3"
|
|
checked={selectedCamera === "Leopards 3"}
|
|
onChange={(e) =>
|
|
setSelectedCamera(e.target.value)
|
|
}
|
|
/>
|
|
Leopards 3
|
|
</label>
|
|
<label
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "6px",
|
|
color: "white",
|
|
justifyContent: "flex-start",
|
|
cursor: "pointer",
|
|
}}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="camera_choice"
|
|
value="Leopards 4"
|
|
checked={selectedCamera === "Leopards 4"}
|
|
onChange={(e) =>
|
|
setSelectedCamera(e.target.value)
|
|
}
|
|
/>
|
|
Leopards 4
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="controls-section" style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: "12px",
|
|
flexWrap: "nowrap",
|
|
padding: "12px 16px",
|
|
background: "linear-gradient(135deg, #2a2d3a 0%, #1f2129 100%)",
|
|
borderRadius: "12px",
|
|
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
|
|
border: "1px solid #3a3f4f",
|
|
overflowX: "auto"
|
|
}}>
|
|
<div className="control-group" style={{ margin: 0, flexShrink: 0 }}>
|
|
<button
|
|
className="drawer-toggle"
|
|
onClick={() => setDrawerOpen(!drawerOpen)}
|
|
style={{
|
|
padding: "8px 12px",
|
|
fontSize: "0.85em",
|
|
minWidth: "70px",
|
|
background: "linear-gradient(135deg, #4a90e2 0%, #357abd 100%)",
|
|
color: "white",
|
|
border: "none",
|
|
borderRadius: "6px",
|
|
cursor: "pointer",
|
|
fontWeight: "500",
|
|
boxShadow: "0 2px 4px rgba(74, 144, 226, 0.3)",
|
|
transition: "all 0.2s ease"
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.target.style.transform = "translateY(-1px)";
|
|
e.target.style.boxShadow = "0 4px 8px rgba(74, 144, 226, 0.4)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.target.style.transform = "translateY(0)";
|
|
e.target.style.boxShadow = "0 2px 4px rgba(74, 144, 226, 0.3)";
|
|
}}
|
|
>
|
|
{drawerOpen ? "✕" : "Options"}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="control-group" style={{
|
|
margin: 0,
|
|
flexShrink: 0,
|
|
background: "rgba(255, 255, 255, 0.05)",
|
|
borderRadius: "6px",
|
|
border: "1px solid rgba(255, 255, 255, 0.1)"
|
|
}}>
|
|
<CustomDateRangePicker
|
|
startDate={startRange}
|
|
endDate={endRange}
|
|
setStartRange={setStartRange}
|
|
setEndRange={setEndRange}
|
|
/>
|
|
</div>
|
|
|
|
<div className="control-group" style={{ margin: 0, flex: "1 1 180px", minWidth: "180px", maxWidth: "300px" }}>
|
|
<input
|
|
type="text"
|
|
placeholder="Enter search query..."
|
|
value={queryText}
|
|
onChange={(e) => setQueryText(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") handleResubmit();
|
|
}}
|
|
style={{
|
|
padding: "10px 12px",
|
|
borderRadius: "6px",
|
|
border: "2px solid #3a3f4f",
|
|
color: "#e1e5e9",
|
|
backgroundColor: "rgba(26, 29, 35, 0.8)",
|
|
width: "100%",
|
|
boxSizing: "border-box",
|
|
fontSize: "0.9em",
|
|
fontWeight: "400",
|
|
outline: "none",
|
|
transition: "all 0.2s ease",
|
|
boxShadow: "inset 0 2px 4px rgba(0, 0, 0, 0.1)"
|
|
}}
|
|
onFocus={(e) => {
|
|
e.target.style.borderColor = "#4a90e2";
|
|
e.target.style.boxShadow = "inset 0 2px 4px rgba(0, 0, 0, 0.1), 0 0 0 3px rgba(74, 144, 226, 0.15)";
|
|
}}
|
|
onBlur={(e) => {
|
|
e.target.style.borderColor = "#3a3f4f";
|
|
e.target.style.boxShadow = "inset 0 2px 4px rgba(0, 0, 0, 0.1)";
|
|
}}
|
|
ref={inputRef}
|
|
/>
|
|
</div>
|
|
|
|
<div className="control-group" style={{
|
|
margin: 0,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "6px",
|
|
color: "#e1e5e9",
|
|
fontSize: "0.85em",
|
|
background: "rgba(255, 255, 255, 0.05)",
|
|
padding: "6px 10px",
|
|
borderRadius: "6px",
|
|
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
flexShrink: 0
|
|
}}>
|
|
<input
|
|
type="checkbox"
|
|
name="do_high_res"
|
|
checked={selectedHighRes}
|
|
onChange={(e) =>
|
|
selectHighResFunc(
|
|
selectedHighRes,
|
|
setSelectedHighRes,
|
|
)
|
|
}
|
|
style={{
|
|
width: "14px",
|
|
height: "14px",
|
|
accentColor: "#4a90e2"
|
|
}}
|
|
/>
|
|
<span style={{ fontWeight: "500", whiteSpace: "nowrap" }}>HD</span>
|
|
</div>
|
|
|
|
<div style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "8px",
|
|
visibility: queryChanged ? "hidden" : "visible",
|
|
background: "rgba(255, 255, 255, 0.05)",
|
|
padding: "6px 12px",
|
|
borderRadius: "6px",
|
|
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
flexShrink: 0
|
|
}}>
|
|
<label style={{
|
|
color: "#e1e5e9",
|
|
fontSize: "0.85em",
|
|
whiteSpace: "nowrap",
|
|
fontWeight: "500"
|
|
}}>
|
|
Threshold:
|
|
</label>
|
|
<input
|
|
type="range"
|
|
min={sliderMin}
|
|
max={sliderMax}
|
|
step={0.001}
|
|
value={sliderValue}
|
|
onChange={(e) => updateDataAndValue(e.target.value)}
|
|
style={{
|
|
width: "80px",
|
|
height: "6px",
|
|
background: "linear-gradient(to right, #3a3f4f 0%, #4a90e2 100%)",
|
|
borderRadius: "3px",
|
|
outline: "none",
|
|
appearance: "none"
|
|
}}
|
|
/>
|
|
<span style={{
|
|
color: "#4a90e2",
|
|
fontSize: "0.8em",
|
|
minWidth: "35px",
|
|
textAlign: "right",
|
|
fontWeight: "600",
|
|
fontFamily: "monospace"
|
|
}}>
|
|
{sliderValue.toFixed(2)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="control-group" style={{
|
|
margin: 0,
|
|
visibility: queryChanged ? "visible" : "hidden",
|
|
flexShrink: 0
|
|
}}>
|
|
<button
|
|
onClick={handleResubmit}
|
|
style={{
|
|
padding: "8px 14px",
|
|
fontSize: "0.85em",
|
|
background: "linear-gradient(135deg, #28a745 0%, #20a73a 100%)",
|
|
color: "white",
|
|
border: "none",
|
|
borderRadius: "6px",
|
|
cursor: "pointer",
|
|
fontWeight: "600",
|
|
boxShadow: "0 2px 4px rgba(40, 167, 69, 0.3)",
|
|
transition: "all 0.2s ease",
|
|
whiteSpace: "nowrap"
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.target.style.transform = "translateY(-1px)";
|
|
e.target.style.boxShadow = "0 4px 8px rgba(40, 167, 69, 0.4)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.target.style.transform = "translateY(0)";
|
|
e.target.style.boxShadow = "0 2px 4px rgba(40, 167, 69, 0.3)";
|
|
}}
|
|
>
|
|
🔄 Resubmit
|
|
</button>
|
|
</div>
|
|
|
|
<div className="control-group" style={{ margin: 0, flexShrink: 0 }}>
|
|
<button
|
|
onClick={videoPlaying ? () => window.open("api/media_download/low") : undefined}
|
|
disabled={!videoPlaying}
|
|
style={{
|
|
padding: "6px 10px",
|
|
fontSize: "0.75em",
|
|
background: videoPlaying
|
|
? "linear-gradient(135deg, #17c671 0%, #15b565 100%)"
|
|
: "linear-gradient(135deg, #6c757d 0%, #5a6169 100%)",
|
|
color: videoPlaying ? "white" : "#adb5bd",
|
|
border: "none",
|
|
borderRadius: "6px",
|
|
cursor: videoPlaying ? "pointer" : "not-allowed",
|
|
opacity: videoPlaying ? 1 : 0.6,
|
|
fontWeight: "500",
|
|
boxShadow: videoPlaying
|
|
? "0 2px 4px rgba(23, 198, 113, 0.3)"
|
|
: "0 1px 2px rgba(108, 117, 125, 0.2)",
|
|
transition: "all 0.2s ease",
|
|
whiteSpace: "nowrap"
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (videoPlaying) {
|
|
e.target.style.transform = "translateY(-1px)";
|
|
e.target.style.boxShadow = "0 4px 8px rgba(23, 198, 113, 0.4)";
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (videoPlaying) {
|
|
e.target.style.transform = "translateY(0)";
|
|
e.target.style.boxShadow = "0 2px 4px rgba(23, 198, 113, 0.3)";
|
|
}
|
|
}}
|
|
>
|
|
DL Low-Res Video
|
|
</button>
|
|
</div>
|
|
<div className="control-group" style={{ margin: 0, flexShrink: 0 }}>
|
|
<button
|
|
onClick={videoPlaying ? () => window.open("api/media_download/high") : undefined}
|
|
disabled={!videoPlaying}
|
|
style={{
|
|
padding: "6px 10px",
|
|
fontSize: "0.75em",
|
|
background: videoPlaying
|
|
? "linear-gradient(135deg, #007bff 0%, #0056b3 100%)"
|
|
: "linear-gradient(135deg, #6c757d 0%, #5a6169 100%)",
|
|
color: videoPlaying ? "white" : "#adb5bd",
|
|
border: "none",
|
|
borderRadius: "6px",
|
|
cursor: videoPlaying ? "pointer" : "not-allowed",
|
|
opacity: videoPlaying ? 1 : 0.6,
|
|
fontWeight: "500",
|
|
boxShadow: videoPlaying
|
|
? "0 2px 4px rgba(0, 123, 255, 0.3)"
|
|
: "0 1px 2px rgba(108, 117, 125, 0.2)",
|
|
transition: "all 0.2s ease",
|
|
whiteSpace: "nowrap"
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (videoPlaying) {
|
|
e.target.style.transform = "translateY(-1px)";
|
|
e.target.style.boxShadow = "0 4px 8px rgba(0, 123, 255, 0.4)";
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (videoPlaying) {
|
|
e.target.style.transform = "translateY(0)";
|
|
e.target.style.boxShadow = "0 2px 4px rgba(0, 123, 255, 0.3)";
|
|
}
|
|
}}
|
|
>
|
|
DL High-Res Video
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="status-section">
|
|
<StatusesDisplayHUD statusMessages={statusMessages} />
|
|
</div>
|
|
|
|
<div className="timeline-section">
|
|
<EmbedTimeline
|
|
chartRef={chartRef}
|
|
data_in={dataResults}
|
|
onTimelineClick={handleTimelineClick}
|
|
authenticatedFetch={authenticatedFetch}
|
|
/>
|
|
</div>
|
|
|
|
<div className="video-section vjs-16-9 vjs-fluid">
|
|
<VideoPlayer
|
|
videoRef={playerRef}
|
|
playerInstanceRef={playerInstanceRef}
|
|
setMarkerTimeFunc={setMarkerValueNonReactive}
|
|
authenticatedFetch={authenticatedFetch}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|