343 lines
12 KiB
JavaScript
343 lines
12 KiB
JavaScript
"use client";
|
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
import EmbedTimeline from "./components/EmbedTimeline";
|
|
import VideoPlayer from "./components/VideoPlayer";
|
|
// import ModernDateRangeSelector from './components/ModernDateRangeSelector';
|
|
import CompactDateRangePicker from "./components/CompactDateRangePicker";
|
|
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 playerInstanceRef = useRef(null);
|
|
// State for the values
|
|
window.chartRef = chartRef;
|
|
window.playerRef = playerRef;
|
|
window.playerInstanceRef = playerInstanceRef;
|
|
// Slider states
|
|
|
|
const [sliderMin, setSliderMin] = useState(0.0);
|
|
const [sliderMax, setSliderMax] = useState(1.0);
|
|
// Date range states
|
|
//
|
|
|
|
const [startRange, setStartRange] = useState(
|
|
new Date(new Date().getTime() - 7 * 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("A clouded leopard and a human");
|
|
const [sliderValue, setSliderValue] = useState(0);
|
|
|
|
// State to track last submitted values
|
|
const [lastSubmitted, setLastSubmitted] = useState({
|
|
startRange,
|
|
endRange,
|
|
sliderValue,
|
|
queryText,
|
|
});
|
|
|
|
// Check if any value has changed
|
|
const hasChanged =
|
|
startRange !== lastSubmitted.startRange ||
|
|
endRange !== lastSubmitted.endRange ||
|
|
sliderValue !== lastSubmitted.sliderValue ||
|
|
queryText !== lastSubmitted.queryText;
|
|
|
|
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") {
|
|
console.log(c_line);
|
|
setStatusMessages((msgs) => [
|
|
...msgs,
|
|
c_line,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
read();
|
|
});
|
|
}
|
|
read();
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error while streaming status:", error);
|
|
});
|
|
};
|
|
// Function to resubmit fetch
|
|
const handleResubmit = (doTestMode = false) => {
|
|
console.log("startRange, endRange:", startRange, endRange);
|
|
console.log("test mode:", doTestMode);
|
|
let startRangeUse;
|
|
let endRangeUse;
|
|
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;
|
|
}
|
|
|
|
console.log("Using date range:", startRangeUse, endRangeUse);
|
|
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);
|
|
setDataResults({ videos: [], breaks: [] });
|
|
|
|
fetch("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({
|
|
startRangeUse,
|
|
endRangeUse,
|
|
sliderValue,
|
|
queryText,
|
|
});
|
|
};
|
|
|
|
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
|
|
);
|
|
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"]
|
|
);
|
|
}
|
|
}
|
|
|
|
// Memoize the timeline click handler
|
|
const handleTimelineClick = useCallback(
|
|
(path, timeoffset) => {
|
|
if (playerRef.current && playerInstanceRef.current) {
|
|
playerInstanceRef.current.src({
|
|
src: "api/" + path,
|
|
type: "video/mp4",
|
|
});
|
|
playerInstanceRef.current.on("loadedmetadata", () => {
|
|
playerInstanceRef.current.currentTime(timeoffset);
|
|
});
|
|
}
|
|
},
|
|
[] // Empty dependency array since it only uses playerRef
|
|
);
|
|
|
|
useEffect(() => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
handleResubmit(params.get("test_mode") === "true");
|
|
}, []);
|
|
|
|
// useEffect(() => {
|
|
// if (startRange && endRange) {
|
|
// handleResubmit();
|
|
// }
|
|
// }, [startRange, endRange]);
|
|
|
|
function pollForResult() {
|
|
const poll = () => {
|
|
fetch("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);
|
|
}
|
|
});
|
|
};
|
|
poll();
|
|
}
|
|
|
|
return (
|
|
<div className="app-container">
|
|
<div className="section-box-horiz">
|
|
<div className="flex-group">
|
|
<CustomDateRangePicker
|
|
startDate={startRange}
|
|
endDate={endRange}
|
|
setStartRange={setStartRange}
|
|
setEndRange={setEndRange}
|
|
/>
|
|
</div>
|
|
<div className="flex-group">
|
|
<input
|
|
type="text"
|
|
placeholder="Enter query"
|
|
value={queryText}
|
|
onChange={(e) => setQueryText(e.target.value)}
|
|
style={{
|
|
marginLeft: "16px",
|
|
marginRight: "16px",
|
|
padding: "8px",
|
|
borderRadius: "4px",
|
|
border: "1px solid #343a40",
|
|
color: "#fff", // Text white
|
|
backgroundColor: "#23272f", // Optional: dark background for contrast
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="flex-group">
|
|
<label
|
|
style={{
|
|
marginLeft: "8px",
|
|
marginRight: "8px",
|
|
color: "#fff",
|
|
}}
|
|
>
|
|
Threshold:
|
|
</label>
|
|
</div>
|
|
<div className="flex-group">
|
|
<input
|
|
type="range"
|
|
min={sliderMin}
|
|
max={sliderMax}
|
|
step={0.001}
|
|
value={sliderValue}
|
|
onChange={(e) => updateDataAndValue(e.target.value)}
|
|
style={{
|
|
width: "120px",
|
|
color: "#fff", // Text white
|
|
backgroundColor: "#23272f", // Optional: dark background for contrast
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="flex-group">
|
|
<span style={{ marginLeft: "8px", color: "#fff" }}>
|
|
{sliderValue.toFixed(2)}
|
|
</span>
|
|
</div>
|
|
<div className="flex-group">
|
|
<button onClick={handleResubmit}>Resubmit</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<StatusesDisplayHUD statusMessages={statusMessages} />
|
|
</div>
|
|
|
|
<div className="timeline-container">
|
|
<EmbedTimeline
|
|
chartRef={chartRef}
|
|
data_in={dataResults}
|
|
onTimelineClick={handleTimelineClick}
|
|
/>
|
|
</div>
|
|
<div className="section-box">
|
|
<VideoPlayer
|
|
videoRef={playerRef}
|
|
playerInstanceRef={playerInstanceRef}
|
|
setMarkerTimeFunc={setMarkerValueNonReactive}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|