diff --git a/SearchFrontend/search_ui/.prettierrc b/SearchFrontend/search_ui/.prettierrc index 222861c..3d6a845 100644 --- a/SearchFrontend/search_ui/.prettierrc +++ b/SearchFrontend/search_ui/.prettierrc @@ -1,4 +1,5 @@ { - "tabWidth": 2, - "useTabs": false + "tabWidth": 4, + "useTabs": false, + "experimentalOperatorPosition": "start" } diff --git a/SearchFrontend/search_ui/package-lock.json b/SearchFrontend/search_ui/package-lock.json index f85c2bf..a4f3059 100644 --- a/SearchFrontend/search_ui/package-lock.json +++ b/SearchFrontend/search_ui/package-lock.json @@ -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", diff --git a/SearchFrontend/search_ui/package.json b/SearchFrontend/search_ui/package.json index 993bd53..f8f70e0 100644 --- a/SearchFrontend/search_ui/package.json +++ b/SearchFrontend/search_ui/package.json @@ -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", diff --git a/SearchFrontend/search_ui/src/App.jsx b/SearchFrontend/search_ui/src/App.jsx index 25b91b7..5c22c88 100644 --- a/SearchFrontend/search_ui/src/App.jsx +++ b/SearchFrontend/search_ui/src/App.jsx @@ -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 ( -
-
-
- -
-
- 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 - }} - /> -
-
- -
-
- updateDataAndValue(e.target.value)} - style={{ - width: "120px", - color: "#fff", // Text white - backgroundColor: "#23272f", // Optional: dark background for contrast - }} - /> -
-
- - {sliderValue.toFixed(2)} - -
-
- -
-
-
- -
+ // 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 ( +
+
+
+ +
+
+ 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 + }} + /> +
+
+ +
+
+ updateDataAndValue(e.target.value)} + style={{ + width: "120px", + color: "#fff", // Text white + backgroundColor: "#23272f", // Optional: dark background for contrast + }} + /> +
+
+ + {sliderValue.toFixed(2)} + +
+
+ +
+
+
+ +
+ +
+ +
+
+ +
+
+ ); } export default App; diff --git a/SearchFrontend/search_ui/src/components/StatusDisplay.css b/SearchFrontend/search_ui/src/components/StatusDisplay.css new file mode 100644 index 0000000..a1d7634 --- /dev/null +++ b/SearchFrontend/search_ui/src/components/StatusDisplay.css @@ -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; + } +} diff --git a/SearchFrontend/search_ui/src/components/StatusDisplay.jsx b/SearchFrontend/search_ui/src/components/StatusDisplay.jsx index 26b29db..fa12460 100644 --- a/SearchFrontend/search_ui/src/components/StatusDisplay.jsx +++ b/SearchFrontend/search_ui/src/components/StatusDisplay.jsx @@ -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 ( -
- {Object.entries(msg).map(([when, messages], idx) => ( - - ))} -
- ); +
+ + {rows.length > 0 && ( + + {headerGroups.map((headerGroup) => ( + + {headerGroup.headers.map((column) => ( + + ))} + + ))} + + )} + + {rows.map((row) => { + prepareRow(row); + return ( + + {row.cells.map((cell) => ( + + ))} + + ); + })} + +
+ {column.render("Header")} +
+ {cell.render("Cell")} +
+
+ ); + + return ( +
+ {Object.entries(msg).map(([when, messages], idx) => ( + + ))} +
+ ); } +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 ( -
- {msg_show} -
- ); - + return ( +
+ {msg_show} +
+ ); } - - - //
threshold)[0] diff --git a/VectorService/vector_service.py b/VectorService/vector_service.py index 4475fa6..c721f50 100644 --- a/VectorService/vector_service.py +++ b/VectorService/vector_service.py @@ -1,3 +1,4 @@ + from typing import Union, Optional, List from pydantic import BaseModel from fastapi import FastAPI, Request, Depends @@ -24,9 +25,9 @@ r = redis.Redis(host='localhost', port=6379, db=15) class VideosPostRequest(BaseModel): - query: str = "A cat and a human", - threshold: float = 0.10, - c_dirs: Optional[List[str]] = None, + query: str = "A cat and a human" + threshold: float = 0.10 + c_dirs: Optional[List[str]] = None task_id: str = 'compute_log' @app.post("/videos.json") @@ -61,9 +62,9 @@ async def videos_json( ] - print(','.join([str(x) for x in c_dirs])) - message = {'task':'SCHEDULED','when':[str(x) for x in c_dirs], 'time':time.time()} - r.rpush(task_id, json.dumps(message)) + # print(','.join([str(x) for x in c_dirs])) + # message = {'task':'SCHEDULED','when':[str(x) for x in c_dirs], 'time':time.time()} + # r.rpush(task_id, json.dumps(message))? for x in c_dirs: