This commit is contained in:
2025-09-17 16:50:06 -04:00
parent 50376f71a8
commit 0fa6025514
8 changed files with 567 additions and 326 deletions

View File

@@ -1,4 +1,5 @@
{
"tabWidth": 2,
"useTabs": false
"tabWidth": 4,
"useTabs": false,
"experimentalOperatorPosition": "start"
}

View File

@@ -1753,9 +1753,9 @@
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="
},
"error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
"requires": {
"is-arrayish": "^0.2.1"
}
@@ -2561,6 +2561,11 @@
"prop-types": "^15.5.4"
}
},
"react-table": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz",
"integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA=="
},
"react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",

View File

@@ -29,6 +29,7 @@
"react-dom": "^19.1.1",
"react-flexbox-grid": "^2.1.2",
"react-split-pane": "^0.1.92",
"react-table": "^7.8.0",
"rsuite": "^5.83.3",
"timelines-chart": "^2.14.2",
"uplot": "^1.6.32",

View File

@@ -9,283 +9,334 @@ 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 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 [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;
// Function to resubmit fetch
const handleResubmit = () => {
// Start streaming status updates
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) {
// console.log("Status:", buffer); // Log any remaining text
}
setStatusMessages([]);
// console.log("Status stream finished");
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()) {
// console.log("Status:", line);
console.log(line)
setStatusMessages((msgs) => [...msgs, JSON.parse(line)]);
}
}
read();
});
}
read();
})
.catch((error) => {
console.error("Error while streaming status:", error);
});
const params = new URLSearchParams();
params.append("startRange", startRange.toISOString());
params.append("endRange", endRange.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) => {
const max_value = Math.max(
...data["videos"].map((vid) => vid["embed_scores"]["score"][1])
);
setSliderMax(max_value);
original_data.current = data;
window.original_data = original_data;
setDataResults(data);
});
setLastSubmitted({ startRange, endRange, 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
const [startRange, setStartRange] = useState(
new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000)
);
setDataResults(newData);
}
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);
function setMarkerValueNonReactive(inputValue) {
let chart = chartRef.current.getEchartsInstance();
let options = chart.getOption();
let mappers = options["mappers"];
// State to track last submitted values
const [lastSubmitted, setLastSubmitted] = useState({
startRange,
endRange,
sliderValue,
queryText,
});
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,
},
// 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,
});
};
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"]
);
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);
}
}
// Memoize the timeline click handler
const handleTimelineClick = useCallback(
(path, timeoffset) => {
console.log("Timeline clicked:", path, timeoffset);
function setMarkerValueNonReactive(inputValue) {
let chart = chartRef.current.getEchartsInstance();
let options = chart.getOption();
let mappers = options["mappers"];
if (playerRef.current && playerInstanceRef.current) {
console.log("Seeking video player to:", path, timeoffset);
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
);
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,
},
};
useEffect(() => {
const params = new URLSearchParams(window.location.search); // id=123
let markLine = {
symbol: ["none", "none"],
data: [vv],
lineStyle: { type: "dashed", color: "#FF0000", width: 2 },
silent: true,
animation: false,
};
if (params.get("test_mode") == "true") {
setStartRange(new Date(new Date().getTime() - 2 * 24 * 60 * 60 * 1000));
setEndRange(new Date(new Date().getTime() - 1 * 24 * 60 * 60 * 1000));
// 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"]
);
}
}
handleResubmit();
}, []);
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>
// 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
);
<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>
);
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;

View File

@@ -0,0 +1,90 @@
/* Container */
.table-container {
margin-top: 24px;
padding: 20px;
background: #23272f;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.10);
overflow-x: auto;
}
/* Table */
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
background: #23272f;
color: #e0e6ed;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 1rem;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
/* Table Header */
th {
background: linear-gradient(90deg, #2c313c 80%, #23272f 100%);
color: #a9b7c6;
font-weight: 600;
padding: 12px 10px;
border-bottom: 2px solid #3a7afe;
text-align: left;
letter-spacing: 0.03em;
position: sticky;
top: 0;
z-index: 2;
}
/* Table Body */
td {
padding: 10px 10px;
border-bottom: 1px solid #343a40;
background: #23272f;
color: #e0e6ed;
vertical-align: middle;
transition: background 0.2s;
}
/* Row hover */
tr:hover td {
background: #2c313c;
}
/* First column highlight */
td:first-child, th:first-child {
font-weight: 500;
color: #3a7afe;
}
/* Last row no border */
tr:last-child td {
border-bottom: none;
}
/* Table column widths */
table th:last-child,
table td:last-child {
width: 30%;
min-width: 120px;
max-width: 400px;
/* Prevent shrinking below 30% on wide screens */
}
table th:not(:last-child),
table td:not(:last-child) {
width: calc(70% / (var(--col-count, 1)));
/* --col-count should be set to (number of columns - 1) in your table element via JS or React */
}
/* Responsive tweaks */
@media (max-width: 700px) {
.table-container {
padding: 8px;
border-radius: 8px;
}
table {
font-size: 0.95rem;
}
th, td {
padding: 8px 4px;
}
}

View File

@@ -1,67 +1,159 @@
import { useTable } from "react-table";
import { useMemo } from "react";
import "./StatusDisplay.css";
export default function StatusesDisplayHUD({ statusMessages }) {
const msg = {};
const dataPre = {};
const columns = [
{ Header: "When", accessor: "WHEN" },
{ Header: "Scheduled", accessor: "SCHEDULED" },
{ Header: "Queued", accessor: "QUEUED" },
{ Header: "Processing", accessor: "VECTOR_CALC" },
{ Header: "Calculating", accessor: "SCORE_CALC" },
{ Header: "Status", accessor: "STATUS" },
];
statusMessages.forEach((m) => {
let when_key = 'other'
if (m['task'] == 'SCHEDULED')
m['when'].forEach(( w ) => { msg[w] = 'Scheduled' })
let when_key = "other";
if (m["task"] == "SCHEDULED")
m["when"].forEach((w) => {
msg[w] = "Scheduled";
dataPre[w] = {
SCHEDULED: "✓",
QUEUED: "",
VECTOR_CALC: "",
SCORE_CALC: "",
STATUS: "",
};
});
else {
if ('when' in m)
when_key = m['when']
msg[when_key] = m['task']
if ("when" in m) when_key = m["when"];
let c_task = m["task"];
let msg_show;
switch (c_task) {
case "SCHEDULED":
msg_show = "Scheduled";
dataPre[when_key]["SCHEDULED"] = "✓";
break;
case "QUEUEING":
msg_show = "In compute queue";
dataPre[when_key]["QUEUED"] = "✓";
break;
case "SCORE_CALC_IN_FOLDER_START":
msg_show = "Calculating Scores";
dataPre[when_key]["VECTOR_CALC"] = "...";
break;
case "VECTOR_CALC_IN_FOLDER_START":
msg_show = "Started processing videos";
dataPre[when_key]["VECTOR_CALC"] = "...";
break;
case "VECTOR_CALC_IN_FOLDER_BUMP":
msg_show =
"Processing videos: " +
m["progress"] +
"/" +
m["how_many"];
dataPre[when_key]["VECTOR_CALC"] =
m["progress"] + "/" + m["how_many"];
break;
case "VECTOR_CALC_IN_FOLDER_DONE":
msg_show = "Finished processing videos";
dataPre[when_key]["VECTOR_CALC"] = "✓";
break;
case "SCORE_CALC_IN_FOLDER_DONE":
msg_show = "Finished calculating scores";
dataPre[when_key]["VECTOR_CALC"] = "✓";
dataPre[when_key]["SCORE_CALC"] = "✓";
break;
default:
msg_show = c_task;
}
msg[when_key] = msg_show;
console.log(when_key);
dataPre[when_key]["STATUS"] = msg_show;
}
});
Object.entries(dataPre).forEach(([k, v]) => {
v["WHEN"] = k;
});
const data = useMemo(() => Object.values(dataPre), [dataPre]);
const columnsMemo = useMemo(() => columns, []);
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
useTable({ columns: columnsMemo, data });
return (
<div>
{Object.entries(msg).map(([when, messages], idx) => (
<StatusDisplay key={when} when={when} message={messages} />
))}
</div>
);
<div className="table-container">
<table
{...getTableProps()}
style={{ "--col-count": columns.length - 1 }}
>
{rows.length > 0 && (
<thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<th {...column.getHeaderProps()}>
{column.render("Header")}
</th>
))}
</tr>
))}
</thead>
)}
<tbody {...getTableBodyProps()}>
{rows.map((row) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => (
<td {...cell.getCellProps()}>
{cell.render("Cell")}
</td>
))}
</tr>
);
})}
</tbody>
</table>
</div>
);
return (
<div>
{Object.entries(msg).map(([when, messages], idx) => (
<StatusDisplay key={when} when={when} message={messages} />
))}
</div>
);
}
export function StatusDisplay({ when, message }) {
let msg_show = "";
export function StatusDisplay({when, message }) {
let msg_show = ''
msg_show = when + ': ' + message
msg_show = when + ": " + message;
return (
<div
className="status-message"
style={{
color: "#fff",
background: "#23272f",
padding: "8px",
margin: "4px 0",
borderRadius: "4px",
minHeight: "20px",
}}
>
{msg_show}
</div>
);
return (
<div
className="status-message"
style={{
color: "#fff",
background: "#23272f",
padding: "8px",
margin: "4px 0",
borderRadius: "4px",
minHeight: "20px",
}}
>
{msg_show}
</div>
);
}
// <div
// className="status-messages"
// style={{