Compare commits

...

2 Commits

4 changed files with 134 additions and 9 deletions

View File

@@ -1,21 +1,17 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Navigate, Route, Routes, useLocation } from "react-router-dom"; import { Navigate, Route, Routes, useLocation } from "react-router-dom";
import AppLayout from "./components/AppLayout"; import AppLayout from "./components/AppLayout";
import DatasetStatusPage from "./pages/DatasetStatus";
import LoginPage from "./pages/Login"; import LoginPage from "./pages/Login";
import UploadPage from "./pages/Upload"; import UploadPage from "./pages/Upload";
import StatPage from "./pages/Stats"; import StatPage from "./pages/Stats";
import { getDocumentTitle } from "./utils/documentTitle";
function App() { function App() {
const location = useLocation(); const location = useLocation();
useEffect(() => { useEffect(() => {
const routeTitles: Record<string, string> = { document.title = getDocumentTitle(location.pathname);
"/login": "Sign In",
"/upload": "Upload Dataset",
"/stats": "Stats",
};
document.title = routeTitles[location.pathname];
}, [location.pathname]); }, [location.pathname]);
return ( return (
@@ -24,6 +20,7 @@ function App() {
<Route path="/" element={<Navigate to="/login" replace />} /> <Route path="/" element={<Navigate to="/login" replace />} />
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/upload" element={<UploadPage />} /> <Route path="/upload" element={<UploadPage />} />
<Route path="/dataset/:datasetId/status" element={<DatasetStatusPage />} />
<Route path="/stats" element={<StatPage />} /> <Route path="/stats" element={<StatPage />} />
</Route> </Route>
</Routes> </Routes>

View File

@@ -0,0 +1,111 @@
import { useEffect, useMemo, useState } from "react";
import axios from "axios";
import { useNavigate, useParams } from "react-router-dom";
import StatsStyling from "../styles/stats_styling";
type DatasetStatusResponse = {
status?: "processing" | "complete" | "error";
status_message?: string | null;
completed_at?: string | null;
};
const styles = StatsStyling;
const API_BASE_URL = "http://localhost:5000";
const DatasetStatusPage = () => {
const navigate = useNavigate();
const { datasetId } = useParams<{ datasetId: string }>();
const [loading, setLoading] = useState(true);
const [status, setStatus] = useState<DatasetStatusResponse["status"]>("processing");
const [statusMessage, setStatusMessage] = useState("");
const parsedDatasetId = useMemo(() => Number(datasetId), [datasetId]);
useEffect(() => {
if (!Number.isInteger(parsedDatasetId) || parsedDatasetId <= 0) {
setLoading(false);
setStatus("error");
setStatusMessage("Invalid dataset id.");
return;
}
let pollTimer: number | undefined;
const pollStatus = async () => {
try {
const response = await axios.get<DatasetStatusResponse>(
`${API_BASE_URL}/dataset/${parsedDatasetId}/status`
);
const nextStatus = response.data.status ?? "processing";
setStatus(nextStatus);
setStatusMessage(String(response.data.status_message ?? ""));
setLoading(false);
if (nextStatus === "complete") {
window.setTimeout(() => {
navigate("/stats", { replace: true });
}, 800);
}
} catch (error: unknown) {
setLoading(false);
setStatus("error");
if (axios.isAxiosError(error)) {
const message = String(error.response?.data?.error || error.message || "Request failed");
setStatusMessage(message);
} else {
setStatusMessage("Unable to fetch dataset status.");
}
}
};
void pollStatus();
pollTimer = window.setInterval(() => {
if (status !== "complete" && status !== "error") {
void pollStatus();
}
}, 2000);
return () => {
if (pollTimer) {
window.clearInterval(pollTimer);
}
};
}, [navigate, parsedDatasetId, status]);
const isProcessing = loading || status === "processing";
const isError = status === "error";
return (
<div style={styles.page}>
<div style={{ ...styles.container, maxWidth: 720 }}>
<div style={{ ...styles.card, marginTop: 28 }}>
<h1 style={{ margin: 0, fontSize: 28, color: "#111827" }}>
{isProcessing ? "Processing dataset..." : isError ? "Dataset processing failed" : "Dataset ready"}
</h1>
<p style={{ ...styles.sectionSubtitle, marginTop: 10 }}>
{isProcessing &&
"Your dataset is being analyzed. This page will redirect to stats automatically once complete."}
{isError && "There was an issue while processing your dataset. Please review the error details."}
{status === "complete" && "Processing complete. Redirecting to your stats now..."}
</p>
<div
style={{
...styles.card,
marginTop: 12,
borderColor: isError ? "rgba(185, 28, 28, 0.28)" : "rgba(0,0,0,0.06)",
background: isError ? "#fff5f5" : "#ffffff",
color: isError ? "#991b1b" : "#374151",
boxShadow: "none",
}}
>
{statusMessage || (isProcessing ? "Waiting for updates from the worker queue..." : "No details provided.")}
</div>
</div>
</div>
</div>
);
};
export default DatasetStatusPage;

View File

@@ -46,12 +46,14 @@ const UploadPage = () => {
}, },
}); });
const datasetId = Number(response.data.dataset_id);
setReturnMessage( setReturnMessage(
`Upload queued successfully (dataset #${response.data.dataset_id}). Redirecting to insights...` `Upload queued successfully (dataset #${datasetId}). Redirecting to processing status...`
); );
setTimeout(() => { setTimeout(() => {
navigate("/stats"); navigate(`/dataset/${datasetId}/status`);
}, 400); }, 400);
} catch (error: unknown) { } catch (error: unknown) {
setHasError(true); setHasError(true);

View File

@@ -0,0 +1,15 @@
const DEFAULT_TITLE = "Ethnograph View";
const STATIC_TITLES: Record<string, string> = {
"/login": "Sign In",
"/upload": "Upload Dataset",
"/stats": "Stats",
};
export const getDocumentTitle = (pathname: string) => {
if (pathname.includes("status")) {
return "Processing Dataset";
}
return STATIC_TITLES[pathname] ?? DEFAULT_TITLE;
};