style: run prettifier plugin on entire frontend
This commit is contained in:
@@ -58,8 +58,8 @@ const AutoScrapePage = () => {
|
||||
if (axios.isAxiosError(requestError)) {
|
||||
setReturnMessage(
|
||||
`Failed to load available sources: ${String(
|
||||
requestError.response?.data?.error || requestError.message
|
||||
)}`
|
||||
requestError.response?.data?.error || requestError.message,
|
||||
)}`,
|
||||
);
|
||||
} else {
|
||||
setReturnMessage("Failed to load available sources.");
|
||||
@@ -70,15 +70,19 @@ const AutoScrapePage = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateSourceConfig = (index: number, field: keyof SourceConfig, value: string) => {
|
||||
const updateSourceConfig = (
|
||||
index: number,
|
||||
field: keyof SourceConfig,
|
||||
value: string,
|
||||
) => {
|
||||
setSourceConfigs((previous) =>
|
||||
previous.map((config, configIndex) =>
|
||||
configIndex === index
|
||||
? field === "sourceName"
|
||||
? { ...config, sourceName: value, search: "", category: "" }
|
||||
: { ...config, [field]: value }
|
||||
: config
|
||||
)
|
||||
: config,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -93,7 +97,9 @@ const AutoScrapePage = () => {
|
||||
};
|
||||
|
||||
const removeSourceConfig = (index: number) => {
|
||||
setSourceConfigs((previous) => previous.filter((_, configIndex) => configIndex !== index));
|
||||
setSourceConfigs((previous) =>
|
||||
previous.filter((_, configIndex) => configIndex !== index),
|
||||
);
|
||||
};
|
||||
|
||||
const autoScrape = async () => {
|
||||
@@ -123,7 +129,9 @@ const AutoScrapePage = () => {
|
||||
return {
|
||||
name: source.sourceName,
|
||||
limit: Number(source.limit || 100),
|
||||
search: supportsSearch(sourceOption) ? source.search.trim() || undefined : undefined,
|
||||
search: supportsSearch(sourceOption)
|
||||
? source.search.trim() || undefined
|
||||
: undefined,
|
||||
category: supportsCategories(sourceOption)
|
||||
? source.category.trim() || undefined
|
||||
: undefined,
|
||||
@@ -131,12 +139,15 @@ const AutoScrapePage = () => {
|
||||
});
|
||||
|
||||
const invalidSource = normalizedSources.find(
|
||||
(source) => !source.name || !Number.isFinite(source.limit) || source.limit <= 0
|
||||
(source) =>
|
||||
!source.name || !Number.isFinite(source.limit) || source.limit <= 0,
|
||||
);
|
||||
|
||||
if (invalidSource) {
|
||||
setHasError(true);
|
||||
setReturnMessage("Every source needs a name and a limit greater than zero.");
|
||||
setReturnMessage(
|
||||
"Every source needs a name and a limit greater than zero.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,13 +166,13 @@ const AutoScrapePage = () => {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const datasetId = Number(response.data.dataset_id);
|
||||
|
||||
setReturnMessage(
|
||||
`Auto scrape queued successfully (dataset #${datasetId}). Redirecting to processing status...`
|
||||
`Auto scrape queued successfully (dataset #${datasetId}). Redirecting to processing status...`,
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -171,7 +182,9 @@ const AutoScrapePage = () => {
|
||||
setHasError(true);
|
||||
if (axios.isAxiosError(requestError)) {
|
||||
const message = String(
|
||||
requestError.response?.data?.error || requestError.message || "Auto scrape failed."
|
||||
requestError.response?.data?.error ||
|
||||
requestError.message ||
|
||||
"Auto scrape failed.",
|
||||
);
|
||||
setReturnMessage(`Auto scrape failed: ${message}`);
|
||||
} else {
|
||||
@@ -189,15 +202,26 @@ const AutoScrapePage = () => {
|
||||
<div>
|
||||
<h1 style={styles.sectionHeaderTitle}>Auto Scrape Dataset</h1>
|
||||
<p style={styles.sectionHeaderSubtitle}>
|
||||
Select sources and scrape settings, then queue processing automatically.
|
||||
Select sources and scrape settings, then queue processing
|
||||
automatically.
|
||||
</p>
|
||||
<p style={{ ...styles.subtleBodyText, marginTop: 6, color: "#9a6700" }}>
|
||||
Warning: Scraping more than 250 posts from any single site can take hours due to rate limits.
|
||||
<p
|
||||
style={{
|
||||
...styles.subtleBodyText,
|
||||
marginTop: 6,
|
||||
color: "#9a6700",
|
||||
}}
|
||||
>
|
||||
Warning: Scraping more than 250 posts from any single site can
|
||||
take hours due to rate limits.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
style={{ ...styles.buttonPrimary, opacity: isSubmitting || isLoadingSources ? 0.75 : 1 }}
|
||||
style={{
|
||||
...styles.buttonPrimary,
|
||||
opacity: isSubmitting || isLoadingSources ? 0.75 : 1,
|
||||
}}
|
||||
onClick={autoScrape}
|
||||
disabled={isSubmitting || isLoadingSources}
|
||||
>
|
||||
@@ -213,8 +237,12 @@ const AutoScrapePage = () => {
|
||||
}}
|
||||
>
|
||||
<div style={{ ...styles.card, gridColumn: "auto" }}>
|
||||
<h2 style={{ ...styles.sectionTitle, color: "#24292f" }}>Dataset Name</h2>
|
||||
<p style={styles.sectionSubtitle}>Use a clear label so you can identify this run later.</p>
|
||||
<h2 style={{ ...styles.sectionTitle, color: "#24292f" }}>
|
||||
Dataset Name
|
||||
</h2>
|
||||
<p style={styles.sectionSubtitle}>
|
||||
Use a clear label so you can identify this run later.
|
||||
</p>
|
||||
<input
|
||||
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||
type="text"
|
||||
@@ -225,97 +253,129 @@ const AutoScrapePage = () => {
|
||||
</div>
|
||||
|
||||
<div style={{ ...styles.card, gridColumn: "auto" }}>
|
||||
<h2 style={{ ...styles.sectionTitle, color: "#24292f" }}>Sources</h2>
|
||||
<h2 style={{ ...styles.sectionTitle, color: "#24292f" }}>
|
||||
Sources
|
||||
</h2>
|
||||
<p style={styles.sectionSubtitle}>
|
||||
Configure source, limit, optional search, and optional category.
|
||||
</p>
|
||||
|
||||
{isLoadingSources && <p style={styles.subtleBodyText}>Loading sources...</p>}
|
||||
{isLoadingSources && (
|
||||
<p style={styles.subtleBodyText}>Loading sources...</p>
|
||||
)}
|
||||
|
||||
{!isLoadingSources && sourceOptions.length === 0 && (
|
||||
<p style={styles.subtleBodyText}>No source connectors are currently available.</p>
|
||||
<p style={styles.subtleBodyText}>
|
||||
No source connectors are currently available.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!isLoadingSources && sourceOptions.length > 0 && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: 10 }}
|
||||
>
|
||||
{sourceConfigs.map((source, index) => {
|
||||
const sourceOption = getSourceOption(source.sourceName);
|
||||
const searchEnabled = supportsSearch(sourceOption);
|
||||
const categoriesEnabled = supportsCategories(sourceOption);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`source-${index}`}
|
||||
style={{
|
||||
border: "1px solid #d0d7de",
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
background: "#f6f8fa",
|
||||
display: "grid",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<select
|
||||
value={source.sourceName}
|
||||
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||
onChange={(event) => updateSourceConfig(index, "sourceName", event.target.value)}
|
||||
<div
|
||||
key={`source-${index}`}
|
||||
style={{
|
||||
border: "1px solid #d0d7de",
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
background: "#f6f8fa",
|
||||
display: "grid",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{sourceOptions.map((option) => (
|
||||
<option key={option.id} value={option.id}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={source.limit}
|
||||
placeholder="Limit"
|
||||
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||
onChange={(event) => updateSourceConfig(index, "limit", event.target.value)}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={source.search}
|
||||
placeholder={
|
||||
searchEnabled
|
||||
? "Search term (optional)"
|
||||
: "Search not supported for this source"
|
||||
}
|
||||
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||
disabled={!searchEnabled}
|
||||
onChange={(event) => updateSourceConfig(index, "search", event.target.value)}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={source.category}
|
||||
placeholder={
|
||||
categoriesEnabled
|
||||
? "Category (optional)"
|
||||
: "Categories not supported for this source"
|
||||
}
|
||||
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||
disabled={!categoriesEnabled}
|
||||
onChange={(event) => updateSourceConfig(index, "category", event.target.value)}
|
||||
/>
|
||||
|
||||
{sourceConfigs.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
style={styles.buttonSecondary}
|
||||
onClick={() => removeSourceConfig(index)}
|
||||
<select
|
||||
value={source.sourceName}
|
||||
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||
onChange={(event) =>
|
||||
updateSourceConfig(
|
||||
index,
|
||||
"sourceName",
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
>
|
||||
Remove source
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{sourceOptions.map((option) => (
|
||||
<option key={option.id} value={option.id}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={source.limit}
|
||||
placeholder="Limit"
|
||||
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||
onChange={(event) =>
|
||||
updateSourceConfig(index, "limit", event.target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={source.search}
|
||||
placeholder={
|
||||
searchEnabled
|
||||
? "Search term (optional)"
|
||||
: "Search not supported for this source"
|
||||
}
|
||||
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||
disabled={!searchEnabled}
|
||||
onChange={(event) =>
|
||||
updateSourceConfig(
|
||||
index,
|
||||
"search",
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={source.category}
|
||||
placeholder={
|
||||
categoriesEnabled
|
||||
? "Category (optional)"
|
||||
: "Categories not supported for this source"
|
||||
}
|
||||
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||
disabled={!categoriesEnabled}
|
||||
onChange={(event) =>
|
||||
updateSourceConfig(
|
||||
index,
|
||||
"category",
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{sourceConfigs.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
style={styles.buttonSecondary}
|
||||
onClick={() => removeSourceConfig(index)}
|
||||
>
|
||||
Remove source
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<button type="button" style={styles.buttonSecondary} onClick={addSourceConfig}>
|
||||
<button
|
||||
type="button"
|
||||
style={styles.buttonSecondary}
|
||||
onClick={addSourceConfig}
|
||||
>
|
||||
Add another source
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,9 @@ const DatasetEditPage = () => {
|
||||
.catch((error: unknown) => {
|
||||
setHasError(true);
|
||||
if (axios.isAxiosError(error)) {
|
||||
setStatusMessage(String(error.response?.data?.error || error.message));
|
||||
setStatusMessage(
|
||||
String(error.response?.data?.error || error.message),
|
||||
);
|
||||
} else {
|
||||
setStatusMessage("Could not get dataset info.");
|
||||
}
|
||||
@@ -61,7 +63,6 @@ const DatasetEditPage = () => {
|
||||
});
|
||||
}, [parsedDatasetId]);
|
||||
|
||||
|
||||
const saveDatasetName = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -87,14 +88,18 @@ const DatasetEditPage = () => {
|
||||
await axios.patch(
|
||||
`${API_BASE_URL}/dataset/${parsedDatasetId}`,
|
||||
{ name: trimmedName },
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
{ headers: { Authorization: `Bearer ${token}` } },
|
||||
);
|
||||
|
||||
navigate("/datasets", { replace: true });
|
||||
} catch (error: unknown) {
|
||||
setHasError(true);
|
||||
if (axios.isAxiosError(error)) {
|
||||
setStatusMessage(String(error.response?.data?.error || error.message || "Save failed."));
|
||||
setStatusMessage(
|
||||
String(
|
||||
error.response?.data?.error || error.message || "Save failed.",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setStatusMessage("Save failed due to an unexpected error.");
|
||||
}
|
||||
@@ -117,17 +122,20 @@ const DatasetEditPage = () => {
|
||||
setHasError(false);
|
||||
setStatusMessage("");
|
||||
|
||||
await axios.delete(
|
||||
`${API_BASE_URL}/dataset/${parsedDatasetId}`,
|
||||
{ headers: { Authorization: `Bearer ${deleteToken}` } }
|
||||
);
|
||||
await axios.delete(`${API_BASE_URL}/dataset/${parsedDatasetId}`, {
|
||||
headers: { Authorization: `Bearer ${deleteToken}` },
|
||||
});
|
||||
|
||||
setIsDeleteModalOpen(false);
|
||||
navigate("/datasets", { replace: true });
|
||||
} catch (error: unknown) {
|
||||
setHasError(true);
|
||||
if (axios.isAxiosError(error)) {
|
||||
setStatusMessage(String(error.response?.data?.error || error.message || "Delete failed."));
|
||||
setStatusMessage(
|
||||
String(
|
||||
error.response?.data?.error || error.message || "Delete failed.",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setStatusMessage("Delete failed due to an unexpected error.");
|
||||
}
|
||||
@@ -142,7 +150,9 @@ const DatasetEditPage = () => {
|
||||
<div style={{ ...styles.card, ...styles.headerBar }}>
|
||||
<div>
|
||||
<h1 style={styles.sectionHeaderTitle}>Edit Dataset</h1>
|
||||
<p style={styles.sectionHeaderSubtitle}>Update the dataset name shown in your datasets list.</p>
|
||||
<p style={styles.sectionHeaderSubtitle}>
|
||||
Update the dataset name shown in your datasets list.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -173,8 +183,8 @@ const DatasetEditPage = () => {
|
||||
style={styles.buttonDanger}
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
disabled={isSaving || isDeleting}
|
||||
>
|
||||
Delete Dataset
|
||||
>
|
||||
Delete Dataset
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -187,15 +197,16 @@ const DatasetEditPage = () => {
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
style={{ ...styles.buttonPrimary, opacity: loading || isSaving ? 0.75 : 1 }}
|
||||
style={{
|
||||
...styles.buttonPrimary,
|
||||
opacity: loading || isSaving ? 0.75 : 1,
|
||||
}}
|
||||
disabled={loading || isSaving || isDeleting}
|
||||
>
|
||||
{isSaving ? "Saving..." : "Save"}
|
||||
</button>
|
||||
|
||||
{loading
|
||||
? "Loading dataset details..."
|
||||
: statusMessage}
|
||||
{loading ? "Loading dataset details..." : statusMessage}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import axios from "axios";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import StatsStyling from "../styles/stats_styling";
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL;
|
||||
|
||||
type DatasetStatusResponse = {
|
||||
status?: "fetching" | "processing" | "complete" | "error";
|
||||
@@ -17,7 +17,8 @@ const DatasetStatusPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { datasetId } = useParams<{ datasetId: string }>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [status, setStatus] = useState<DatasetStatusResponse["status"]>("processing");
|
||||
const [status, setStatus] =
|
||||
useState<DatasetStatusResponse["status"]>("processing");
|
||||
const [statusMessage, setStatusMessage] = useState("");
|
||||
const parsedDatasetId = useMemo(() => Number(datasetId), [datasetId]);
|
||||
|
||||
@@ -34,7 +35,7 @@ const DatasetStatusPage = () => {
|
||||
const pollStatus = async () => {
|
||||
try {
|
||||
const response = await axios.get<DatasetStatusResponse>(
|
||||
`${API_BASE_URL}/dataset/${parsedDatasetId}/status`
|
||||
`${API_BASE_URL}/dataset/${parsedDatasetId}/status`,
|
||||
);
|
||||
|
||||
const nextStatus = response.data.status ?? "processing";
|
||||
@@ -51,7 +52,9 @@ const DatasetStatusPage = () => {
|
||||
setLoading(false);
|
||||
setStatus("error");
|
||||
if (axios.isAxiosError(error)) {
|
||||
const message = String(error.response?.data?.error || error.message || "Request failed");
|
||||
const message = String(
|
||||
error.response?.data?.error || error.message || "Request failed",
|
||||
);
|
||||
setStatusMessage(message);
|
||||
} else {
|
||||
setStatusMessage("Unable to fetch dataset status.");
|
||||
@@ -73,7 +76,8 @@ const DatasetStatusPage = () => {
|
||||
};
|
||||
}, [navigate, parsedDatasetId, status]);
|
||||
|
||||
const isProcessing = loading || status === "fetching" || status === "processing";
|
||||
const isProcessing =
|
||||
loading || status === "fetching" || status === "processing";
|
||||
const isError = status === "error";
|
||||
|
||||
return (
|
||||
@@ -81,26 +85,37 @@ const DatasetStatusPage = () => {
|
||||
<div style={styles.containerNarrow}>
|
||||
<div style={{ ...styles.card, marginTop: 28 }}>
|
||||
<h1 style={styles.sectionHeaderTitle}>
|
||||
{isProcessing ? "Processing dataset..." : isError ? "Dataset processing failed" : "Dataset ready"}
|
||||
{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..."}
|
||||
{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,
|
||||
...styles.statusMessageCard,
|
||||
borderColor: isError ? "rgba(185, 28, 28, 0.28)" : "rgba(0,0,0,0.06)",
|
||||
borderColor: isError
|
||||
? "rgba(185, 28, 28, 0.28)"
|
||||
: "rgba(0,0,0,0.06)",
|
||||
background: isError ? "#fff5f5" : "#ffffff",
|
||||
color: isError ? "#991b1b" : "#374151",
|
||||
}}
|
||||
>
|
||||
{statusMessage || (isProcessing ? "Waiting for updates from the worker queue..." : "No details provided.")}
|
||||
{statusMessage ||
|
||||
(isProcessing
|
||||
? "Waiting for updates from the worker queue..."
|
||||
: "No details provided.")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,9 @@ const DatasetsPage = () => {
|
||||
})
|
||||
.catch((requestError: unknown) => {
|
||||
if (axios.isAxiosError(requestError)) {
|
||||
setError(String(requestError.response?.data?.error || requestError.message));
|
||||
setError(
|
||||
String(requestError.response?.data?.error || requestError.message),
|
||||
);
|
||||
} else {
|
||||
setError("Failed to load datasets.");
|
||||
}
|
||||
@@ -61,13 +63,28 @@ const DatasetsPage = () => {
|
||||
</div>
|
||||
|
||||
<div style={styles.loadingSkeleton}>
|
||||
<div style={{ ...styles.loadingSkeletonLine, ...styles.loadingSkeletonLineLong }} />
|
||||
<div style={{ ...styles.loadingSkeletonLine, ...styles.loadingSkeletonLineMed }} />
|
||||
<div style={{ ...styles.loadingSkeletonLine, ...styles.loadingSkeletonLineShort }} />
|
||||
<div
|
||||
style={{
|
||||
...styles.loadingSkeletonLine,
|
||||
...styles.loadingSkeletonLineLong,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
...styles.loadingSkeletonLine,
|
||||
...styles.loadingSkeletonLineMed,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
...styles.loadingSkeletonLine,
|
||||
...styles.loadingSkeletonLineShort,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -81,7 +98,11 @@ const DatasetsPage = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div style={styles.controlsWrapped}>
|
||||
<button type="button" style={styles.buttonPrimary} onClick={() => navigate("/upload")}>
|
||||
<button
|
||||
type="button"
|
||||
style={styles.buttonPrimary}
|
||||
onClick={() => navigate("/upload")}
|
||||
>
|
||||
Upload New Dataset
|
||||
</button>
|
||||
<button
|
||||
@@ -116,20 +137,25 @@ const DatasetsPage = () => {
|
||||
)}
|
||||
|
||||
{!error && datasets.length > 0 && (
|
||||
<div style={{ ...styles.card, marginTop: 14, padding: 0, overflow: "hidden" }}>
|
||||
<div
|
||||
style={{
|
||||
...styles.card,
|
||||
marginTop: 14,
|
||||
padding: 0,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<ul style={styles.listNoBullets}>
|
||||
{datasets.map((dataset) => {
|
||||
const isComplete = dataset.status === "complete" || dataset.status === "error";
|
||||
const isComplete =
|
||||
dataset.status === "complete" || dataset.status === "error";
|
||||
const editPath = `/dataset/${dataset.id}/edit`;
|
||||
const targetPath = isComplete
|
||||
? `/dataset/${dataset.id}/stats`
|
||||
: `/dataset/${dataset.id}/status`;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={dataset.id}
|
||||
style={styles.datasetListItem}
|
||||
>
|
||||
<li key={dataset.id} style={styles.datasetListItem}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={styles.datasetName}>
|
||||
{dataset.name || `Dataset #${dataset.id}`}
|
||||
@@ -145,19 +171,23 @@ const DatasetsPage = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{ isComplete &&
|
||||
{isComplete && (
|
||||
<button
|
||||
type="button"
|
||||
style={{...styles.buttonSecondary, "margin": "5px"}}
|
||||
style={{ ...styles.buttonSecondary, margin: "5px" }}
|
||||
onClick={() => navigate(editPath)}
|
||||
>
|
||||
Edit Dataset
|
||||
</button>
|
||||
}
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
style={isComplete ? styles.buttonPrimary : styles.buttonSecondary}
|
||||
style={
|
||||
isComplete
|
||||
? styles.buttonPrimary
|
||||
: styles.buttonSecondary
|
||||
}
|
||||
onClick={() => navigate(targetPath)}
|
||||
>
|
||||
{isComplete ? "Open stats" : "View status"}
|
||||
|
||||
@@ -3,7 +3,7 @@ import axios from "axios";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import StatsStyling from "../styles/stats_styling";
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL;
|
||||
|
||||
const styles = StatsStyling;
|
||||
|
||||
@@ -44,13 +44,17 @@ const LoginPage = () => {
|
||||
|
||||
try {
|
||||
if (isRegisterMode) {
|
||||
await axios.post(`${API_BASE_URL}/register`, { username, email, password });
|
||||
await axios.post(`${API_BASE_URL}/register`, {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
});
|
||||
setInfo("Account created. You can now sign in.");
|
||||
setIsRegisterMode(false);
|
||||
} else {
|
||||
const response = await axios.post<{ access_token: string }>(
|
||||
`${API_BASE_URL}/login`,
|
||||
{ username, password }
|
||||
{ username, password },
|
||||
);
|
||||
|
||||
const token = response.data.access_token;
|
||||
@@ -61,7 +65,11 @@ const LoginPage = () => {
|
||||
} catch (requestError: unknown) {
|
||||
if (axios.isAxiosError(requestError)) {
|
||||
setError(
|
||||
String(requestError.response?.data?.error || requestError.message || "Request failed")
|
||||
String(
|
||||
requestError.response?.data?.error ||
|
||||
requestError.message ||
|
||||
"Request failed",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setError("Unexpected error occurred.");
|
||||
@@ -73,90 +81,86 @@ const LoginPage = () => {
|
||||
|
||||
return (
|
||||
<div style={styles.containerAuth}>
|
||||
<div style={{ ...styles.card, ...styles.authCard }}>
|
||||
<div style={styles.headingBlock}>
|
||||
<h1 style={styles.headingXl}>
|
||||
{isRegisterMode ? "Create your account" : "Welcome back"}
|
||||
</h1>
|
||||
<p style={styles.mutedText}>
|
||||
{isRegisterMode
|
||||
? "Register to start uploading and exploring your dataset insights."
|
||||
: "Sign in to continue to your analytics workspace."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} style={styles.authForm}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
style={{ ...styles.input, ...styles.authControl }}
|
||||
value={username}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
{isRegisterMode && (
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
style={{ ...styles.input, ...styles.authControl }}
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
style={{ ...styles.input, ...styles.authControl }}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
style={{ ...styles.buttonPrimary, ...styles.authControl, marginTop: 2 }}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading
|
||||
? "Please wait..."
|
||||
: isRegisterMode
|
||||
? "Create account"
|
||||
: "Sign in"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{error && (
|
||||
<p style={styles.authErrorText}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{info && (
|
||||
<p style={styles.authInfoText}>
|
||||
{info}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div style={styles.authSwitchRow}>
|
||||
<span style={styles.authSwitchLabel}>
|
||||
{isRegisterMode ? "Already have an account?" : "New here?"}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
style={styles.authSwitchButton}
|
||||
onClick={() => {
|
||||
setError("");
|
||||
setInfo("");
|
||||
setIsRegisterMode((value) => !value);
|
||||
}}
|
||||
>
|
||||
{isRegisterMode ? "Switch to sign in" : "Create account"}
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ ...styles.card, ...styles.authCard }}>
|
||||
<div style={styles.headingBlock}>
|
||||
<h1 style={styles.headingXl}>
|
||||
{isRegisterMode ? "Create your account" : "Welcome back"}
|
||||
</h1>
|
||||
<p style={styles.mutedText}>
|
||||
{isRegisterMode
|
||||
? "Register to start uploading and exploring your dataset insights."
|
||||
: "Sign in to continue to your analytics workspace."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} style={styles.authForm}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
style={{ ...styles.input, ...styles.authControl }}
|
||||
value={username}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
{isRegisterMode && (
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
style={{ ...styles.input, ...styles.authControl }}
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
style={{ ...styles.input, ...styles.authControl }}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
...styles.buttonPrimary,
|
||||
...styles.authControl,
|
||||
marginTop: 2,
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading
|
||||
? "Please wait..."
|
||||
: isRegisterMode
|
||||
? "Create account"
|
||||
: "Sign in"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{error && <p style={styles.authErrorText}>{error}</p>}
|
||||
|
||||
{info && <p style={styles.authInfoText}>{info}</p>}
|
||||
|
||||
<div style={styles.authSwitchRow}>
|
||||
<span style={styles.authSwitchLabel}>
|
||||
{isRegisterMode ? "Already have an account?" : "New here?"}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
style={styles.authSwitchButton}
|
||||
onClick={() => {
|
||||
setError("");
|
||||
setInfo("");
|
||||
setIsRegisterMode((value) => !value);
|
||||
}}
|
||||
>
|
||||
{isRegisterMode ? "Switch to sign in" : "Create account"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,47 +9,59 @@ import LinguisticStats from "../components/LinguisticStats";
|
||||
import InteractionalStats from "../components/InteractionalStats";
|
||||
import CulturalStats from "../components/CulturalStats";
|
||||
|
||||
import {
|
||||
type SummaryResponse,
|
||||
type UserAnalysisResponse,
|
||||
import {
|
||||
type SummaryResponse,
|
||||
type UserAnalysisResponse,
|
||||
type TimeAnalysisResponse,
|
||||
type ContentAnalysisResponse,
|
||||
type UserEndpointResponse,
|
||||
type LinguisticAnalysisResponse,
|
||||
type EmotionalAnalysisResponse,
|
||||
type InteractionAnalysisResponse,
|
||||
type CulturalAnalysisResponse
|
||||
} from '../types/ApiTypes'
|
||||
type CulturalAnalysisResponse,
|
||||
} from "../types/ApiTypes";
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL;
|
||||
const styles = StatsStyling;
|
||||
const DELETED_USERS = ["[deleted]"];
|
||||
|
||||
const isDeletedUser = (value: string | null | undefined) => (
|
||||
DELETED_USERS.includes((value ?? "").trim().toLowerCase())
|
||||
);
|
||||
const isDeletedUser = (value: string | null | undefined) =>
|
||||
DELETED_USERS.includes((value ?? "").trim().toLowerCase());
|
||||
|
||||
const StatPage = () => {
|
||||
const { datasetId: routeDatasetId } = useParams<{ datasetId: string }>();
|
||||
const [error, setError] = useState('');
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeView, setActiveView] = useState<"summary" | "emotional" | "user" | "linguistic" | "interactional" | "cultural">("summary");
|
||||
const [activeView, setActiveView] = useState<
|
||||
| "summary"
|
||||
| "emotional"
|
||||
| "user"
|
||||
| "linguistic"
|
||||
| "interactional"
|
||||
| "cultural"
|
||||
>("summary");
|
||||
|
||||
const [userData, setUserData] = useState<UserAnalysisResponse | null>(null);
|
||||
const [timeData, setTimeData] = useState<TimeAnalysisResponse | null>(null);
|
||||
const [contentData, setContentData] = useState<ContentAnalysisResponse | null>(null);
|
||||
const [linguisticData, setLinguisticData] = useState<LinguisticAnalysisResponse | null>(null);
|
||||
const [interactionData, setInteractionData] = useState<InteractionAnalysisResponse | null>(null);
|
||||
const [culturalData, setCulturalData] = useState<CulturalAnalysisResponse | null>(null);
|
||||
const [contentData, setContentData] =
|
||||
useState<ContentAnalysisResponse | null>(null);
|
||||
const [linguisticData, setLinguisticData] =
|
||||
useState<LinguisticAnalysisResponse | null>(null);
|
||||
const [interactionData, setInteractionData] =
|
||||
useState<InteractionAnalysisResponse | null>(null);
|
||||
const [culturalData, setCulturalData] =
|
||||
useState<CulturalAnalysisResponse | null>(null);
|
||||
const [summary, setSummary] = useState<SummaryResponse | null>(null);
|
||||
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const beforeDateRef = useRef<HTMLInputElement>(null);
|
||||
const afterDateRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const parsedDatasetId = Number(routeDatasetId ?? "");
|
||||
const datasetId = Number.isInteger(parsedDatasetId) && parsedDatasetId > 0 ? parsedDatasetId : null;
|
||||
const datasetId =
|
||||
Number.isInteger(parsedDatasetId) && parsedDatasetId > 0
|
||||
? parsedDatasetId
|
||||
: null;
|
||||
|
||||
const getFilterParams = () => {
|
||||
const params: Record<string, string> = {};
|
||||
@@ -99,112 +111,147 @@ const StatPage = () => {
|
||||
setLoading(true);
|
||||
|
||||
Promise.all([
|
||||
axios.get<TimeAnalysisResponse>(`${API_BASE_URL}/dataset/${datasetId}/temporal`, {
|
||||
params,
|
||||
headers: authHeaders,
|
||||
}),
|
||||
axios.get<UserEndpointResponse>(`${API_BASE_URL}/dataset/${datasetId}/user`, {
|
||||
params,
|
||||
headers: authHeaders,
|
||||
}),
|
||||
axios.get<LinguisticAnalysisResponse>(`${API_BASE_URL}/dataset/${datasetId}/linguistic`, {
|
||||
params,
|
||||
headers: authHeaders,
|
||||
}),
|
||||
axios.get<EmotionalAnalysisResponse>(`${API_BASE_URL}/dataset/${datasetId}/emotional`, {
|
||||
params,
|
||||
headers: authHeaders,
|
||||
}),
|
||||
axios.get<InteractionAnalysisResponse>(`${API_BASE_URL}/dataset/${datasetId}/interactional`, {
|
||||
params,
|
||||
headers: authHeaders,
|
||||
}),
|
||||
axios.get<SummaryResponse>(`${API_BASE_URL}/dataset/${datasetId}/summary`, {
|
||||
params,
|
||||
headers: authHeaders,
|
||||
}),
|
||||
axios.get<CulturalAnalysisResponse>(`${API_BASE_URL}/dataset/${datasetId}/cultural`, {
|
||||
params,
|
||||
headers: authHeaders,
|
||||
}),
|
||||
])
|
||||
.then(([timeRes, userRes, linguisticRes, emotionalRes, interactionRes, summaryRes, culturalRes]) => {
|
||||
const usersList = userRes.data.users ?? [];
|
||||
const topUsersList = userRes.data.top_users ?? [];
|
||||
const interactionGraphRaw = interactionRes.data?.interaction_graph ?? {};
|
||||
const topPairsRaw = interactionRes.data?.top_interaction_pairs ?? [];
|
||||
axios.get<TimeAnalysisResponse>(
|
||||
`${API_BASE_URL}/dataset/${datasetId}/temporal`,
|
||||
{
|
||||
params,
|
||||
headers: authHeaders,
|
||||
},
|
||||
),
|
||||
axios.get<UserEndpointResponse>(
|
||||
`${API_BASE_URL}/dataset/${datasetId}/user`,
|
||||
{
|
||||
params,
|
||||
headers: authHeaders,
|
||||
},
|
||||
),
|
||||
axios.get<LinguisticAnalysisResponse>(
|
||||
`${API_BASE_URL}/dataset/${datasetId}/linguistic`,
|
||||
{
|
||||
params,
|
||||
headers: authHeaders,
|
||||
},
|
||||
),
|
||||
axios.get<EmotionalAnalysisResponse>(
|
||||
`${API_BASE_URL}/dataset/${datasetId}/emotional`,
|
||||
{
|
||||
params,
|
||||
headers: authHeaders,
|
||||
},
|
||||
),
|
||||
axios.get<InteractionAnalysisResponse>(
|
||||
`${API_BASE_URL}/dataset/${datasetId}/interactional`,
|
||||
{
|
||||
params,
|
||||
headers: authHeaders,
|
||||
},
|
||||
),
|
||||
axios.get<SummaryResponse>(
|
||||
`${API_BASE_URL}/dataset/${datasetId}/summary`,
|
||||
{
|
||||
params,
|
||||
headers: authHeaders,
|
||||
},
|
||||
),
|
||||
axios.get<CulturalAnalysisResponse>(
|
||||
`${API_BASE_URL}/dataset/${datasetId}/cultural`,
|
||||
{
|
||||
params,
|
||||
headers: authHeaders,
|
||||
},
|
||||
),
|
||||
])
|
||||
.then(
|
||||
([
|
||||
timeRes,
|
||||
userRes,
|
||||
linguisticRes,
|
||||
emotionalRes,
|
||||
interactionRes,
|
||||
summaryRes,
|
||||
culturalRes,
|
||||
]) => {
|
||||
const usersList = userRes.data.users ?? [];
|
||||
const topUsersList = userRes.data.top_users ?? [];
|
||||
const interactionGraphRaw =
|
||||
interactionRes.data?.interaction_graph ?? {};
|
||||
const topPairsRaw = interactionRes.data?.top_interaction_pairs ?? [];
|
||||
|
||||
const filteredUsers: typeof usersList = [];
|
||||
for (const user of usersList) {
|
||||
if (isDeletedUser(user.author)) continue;
|
||||
filteredUsers.push(user);
|
||||
}
|
||||
|
||||
const filteredTopUsers: typeof topUsersList = [];
|
||||
for (const user of topUsersList) {
|
||||
if (isDeletedUser(user.author)) continue;
|
||||
filteredTopUsers.push(user);
|
||||
}
|
||||
|
||||
const filteredInteractionGraph: Record<string, Record<string, number>> = {};
|
||||
for (const [source, targets] of Object.entries(interactionGraphRaw)) {
|
||||
if (isDeletedUser(source)) {
|
||||
continue;
|
||||
const filteredUsers: typeof usersList = [];
|
||||
for (const user of usersList) {
|
||||
if (isDeletedUser(user.author)) continue;
|
||||
filteredUsers.push(user);
|
||||
}
|
||||
|
||||
const nextTargets: Record<string, number> = {};
|
||||
for (const [target, count] of Object.entries(targets)) {
|
||||
if (isDeletedUser(target)) {
|
||||
const filteredTopUsers: typeof topUsersList = [];
|
||||
for (const user of topUsersList) {
|
||||
if (isDeletedUser(user.author)) continue;
|
||||
filteredTopUsers.push(user);
|
||||
}
|
||||
|
||||
const filteredInteractionGraph: Record<
|
||||
string,
|
||||
Record<string, number>
|
||||
> = {};
|
||||
for (const [source, targets] of Object.entries(interactionGraphRaw)) {
|
||||
if (isDeletedUser(source)) {
|
||||
continue;
|
||||
}
|
||||
nextTargets[target] = count;
|
||||
|
||||
const nextTargets: Record<string, number> = {};
|
||||
for (const [target, count] of Object.entries(targets)) {
|
||||
if (isDeletedUser(target)) {
|
||||
continue;
|
||||
}
|
||||
nextTargets[target] = count;
|
||||
}
|
||||
|
||||
filteredInteractionGraph[source] = nextTargets;
|
||||
}
|
||||
|
||||
filteredInteractionGraph[source] = nextTargets;
|
||||
}
|
||||
|
||||
const filteredTopInteractionPairs: typeof topPairsRaw = [];
|
||||
for (const pairEntry of topPairsRaw) {
|
||||
const pair = pairEntry[0];
|
||||
const source = pair[0];
|
||||
const target = pair[1];
|
||||
if (isDeletedUser(source) || isDeletedUser(target)) {
|
||||
continue;
|
||||
const filteredTopInteractionPairs: typeof topPairsRaw = [];
|
||||
for (const pairEntry of topPairsRaw) {
|
||||
const pair = pairEntry[0];
|
||||
const source = pair[0];
|
||||
const target = pair[1];
|
||||
if (isDeletedUser(source) || isDeletedUser(target)) {
|
||||
continue;
|
||||
}
|
||||
filteredTopInteractionPairs.push(pairEntry);
|
||||
}
|
||||
filteredTopInteractionPairs.push(pairEntry);
|
||||
}
|
||||
|
||||
const combinedUserData: UserAnalysisResponse = {
|
||||
...userRes.data,
|
||||
users: filteredUsers,
|
||||
top_users: filteredTopUsers,
|
||||
interaction_graph: filteredInteractionGraph,
|
||||
};
|
||||
const combinedUserData: UserAnalysisResponse = {
|
||||
...userRes.data,
|
||||
users: filteredUsers,
|
||||
top_users: filteredTopUsers,
|
||||
interaction_graph: filteredInteractionGraph,
|
||||
};
|
||||
|
||||
const combinedContentData: ContentAnalysisResponse = {
|
||||
...linguisticRes.data,
|
||||
...emotionalRes.data,
|
||||
};
|
||||
const combinedContentData: ContentAnalysisResponse = {
|
||||
...linguisticRes.data,
|
||||
...emotionalRes.data,
|
||||
};
|
||||
|
||||
const filteredInteractionData: InteractionAnalysisResponse = {
|
||||
...interactionRes.data,
|
||||
interaction_graph: filteredInteractionGraph,
|
||||
top_interaction_pairs: filteredTopInteractionPairs,
|
||||
};
|
||||
const filteredInteractionData: InteractionAnalysisResponse = {
|
||||
...interactionRes.data,
|
||||
interaction_graph: filteredInteractionGraph,
|
||||
top_interaction_pairs: filteredTopInteractionPairs,
|
||||
};
|
||||
|
||||
const filteredSummary: SummaryResponse = {
|
||||
...summaryRes.data,
|
||||
unique_users: filteredUsers.length,
|
||||
};
|
||||
const filteredSummary: SummaryResponse = {
|
||||
...summaryRes.data,
|
||||
unique_users: filteredUsers.length,
|
||||
};
|
||||
|
||||
setUserData(combinedUserData);
|
||||
setTimeData(timeRes.data || null);
|
||||
setContentData(combinedContentData);
|
||||
setLinguisticData(linguisticRes.data || null);
|
||||
setInteractionData(filteredInteractionData || null);
|
||||
setCulturalData(culturalRes.data || null);
|
||||
setSummary(filteredSummary || null);
|
||||
})
|
||||
setUserData(combinedUserData);
|
||||
setTimeData(timeRes.data || null);
|
||||
setContentData(combinedContentData);
|
||||
setLinguisticData(linguisticRes.data || null);
|
||||
setInteractionData(filteredInteractionData || null);
|
||||
setCulturalData(culturalRes.data || null);
|
||||
setSummary(filteredSummary || null);
|
||||
},
|
||||
)
|
||||
.catch((e) => setError("Failed to load statistics: " + String(e)))
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
@@ -233,7 +280,7 @@ const StatPage = () => {
|
||||
return;
|
||||
}
|
||||
getStats();
|
||||
}, [datasetId])
|
||||
}, [datasetId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -243,155 +290,199 @@ const StatPage = () => {
|
||||
<div style={styles.loadingSpinner} />
|
||||
<div>
|
||||
<h2 style={styles.loadingTitle}>Loading analytics</h2>
|
||||
<p style={styles.loadingSubtitle}>Fetching summary, timeline, user, and content insights.</p>
|
||||
<p style={styles.loadingSubtitle}>
|
||||
Fetching summary, timeline, user, and content insights.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.loadingSkeleton}>
|
||||
<div style={{ ...styles.loadingSkeletonLine, ...styles.loadingSkeletonLineLong }} />
|
||||
<div style={{ ...styles.loadingSkeletonLine, ...styles.loadingSkeletonLineMed }} />
|
||||
<div style={{ ...styles.loadingSkeletonLine, ...styles.loadingSkeletonLineShort }} />
|
||||
<div
|
||||
style={{
|
||||
...styles.loadingSkeletonLine,
|
||||
...styles.loadingSkeletonLineLong,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
...styles.loadingSkeletonLine,
|
||||
...styles.loadingSkeletonLineMed,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
...styles.loadingSkeletonLine,
|
||||
...styles.loadingSkeletonLineShort,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (error) return <p style={{...styles.page}}>{error}</p>;
|
||||
if (error) return <p style={{ ...styles.page }}>{error}</p>;
|
||||
|
||||
return (
|
||||
<div style={styles.page}>
|
||||
<div style={{ ...styles.container, ...styles.card, ...styles.headerBar }}>
|
||||
<div style={styles.controls}>
|
||||
<input
|
||||
type="text"
|
||||
id="query"
|
||||
ref={searchInputRef}
|
||||
placeholder="Search events..."
|
||||
style={styles.input}
|
||||
/>
|
||||
return (
|
||||
<div style={styles.page}>
|
||||
<div style={{ ...styles.container, ...styles.card, ...styles.headerBar }}>
|
||||
<div style={styles.controls}>
|
||||
<input
|
||||
type="text"
|
||||
id="query"
|
||||
ref={searchInputRef}
|
||||
placeholder="Search events..."
|
||||
style={styles.input}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="date"
|
||||
ref={beforeDateRef}
|
||||
placeholder="Search before date"
|
||||
style={styles.input}
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
ref={beforeDateRef}
|
||||
placeholder="Search before date"
|
||||
style={styles.input}
|
||||
/>
|
||||
|
||||
<input
|
||||
<input
|
||||
type="date"
|
||||
ref={afterDateRef}
|
||||
placeholder="Search before date"
|
||||
style={styles.input}
|
||||
/>
|
||||
/>
|
||||
|
||||
<button onClick={onSubmitFilters} style={styles.buttonPrimary}>
|
||||
Search
|
||||
</button>
|
||||
<button onClick={onSubmitFilters} style={styles.buttonPrimary}>
|
||||
Search
|
||||
</button>
|
||||
|
||||
<button onClick={resetFilters} style={styles.buttonSecondary}>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={styles.dashboardMeta}>Analytics Dashboard</div>
|
||||
<div style={styles.dashboardMeta}>Dataset #{datasetId ?? "-"}</div>
|
||||
<button onClick={resetFilters} style={styles.buttonSecondary}>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ ...styles.container, ...styles.tabsRow, justifyContent: "center" }}>
|
||||
<button
|
||||
onClick={() => setActiveView("summary")}
|
||||
style={activeView === "summary" ? styles.buttonPrimary : styles.buttonSecondary}
|
||||
>
|
||||
Summary
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView("emotional")}
|
||||
style={activeView === "emotional" ? styles.buttonPrimary : styles.buttonSecondary}
|
||||
>
|
||||
Emotional
|
||||
</button>
|
||||
<div style={styles.dashboardMeta}>Analytics Dashboard</div>
|
||||
<div style={styles.dashboardMeta}>Dataset #{datasetId ?? "-"}</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveView("user")}
|
||||
style={activeView === "user" ? styles.buttonPrimary : styles.buttonSecondary}
|
||||
<div
|
||||
style={{
|
||||
...styles.container,
|
||||
...styles.tabsRow,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
Users
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView("linguistic")}
|
||||
style={activeView === "linguistic" ? styles.buttonPrimary : styles.buttonSecondary}
|
||||
>
|
||||
Linguistic
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView("interactional")}
|
||||
style={activeView === "interactional" ? styles.buttonPrimary : styles.buttonSecondary}
|
||||
>
|
||||
Interactional
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView("cultural")}
|
||||
style={activeView === "cultural" ? styles.buttonPrimary : styles.buttonSecondary}
|
||||
>
|
||||
Cultural
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView("summary")}
|
||||
style={
|
||||
activeView === "summary"
|
||||
? styles.buttonPrimary
|
||||
: styles.buttonSecondary
|
||||
}
|
||||
>
|
||||
Summary
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView("emotional")}
|
||||
style={
|
||||
activeView === "emotional"
|
||||
? styles.buttonPrimary
|
||||
: styles.buttonSecondary
|
||||
}
|
||||
>
|
||||
Emotional
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveView("user")}
|
||||
style={
|
||||
activeView === "user"
|
||||
? styles.buttonPrimary
|
||||
: styles.buttonSecondary
|
||||
}
|
||||
>
|
||||
Users
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView("linguistic")}
|
||||
style={
|
||||
activeView === "linguistic"
|
||||
? styles.buttonPrimary
|
||||
: styles.buttonSecondary
|
||||
}
|
||||
>
|
||||
Linguistic
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView("interactional")}
|
||||
style={
|
||||
activeView === "interactional"
|
||||
? styles.buttonPrimary
|
||||
: styles.buttonSecondary
|
||||
}
|
||||
>
|
||||
Interactional
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView("cultural")}
|
||||
style={
|
||||
activeView === "cultural"
|
||||
? styles.buttonPrimary
|
||||
: styles.buttonSecondary
|
||||
}
|
||||
>
|
||||
Cultural
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeView === "summary" && (
|
||||
<SummaryStats
|
||||
userData={userData}
|
||||
timeData={timeData}
|
||||
contentData={contentData}
|
||||
summary={summary}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeView === "emotional" && contentData && (
|
||||
<EmotionalStats contentData={contentData} />
|
||||
)}
|
||||
|
||||
{activeView === "emotional" && !contentData && (
|
||||
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
||||
No emotional data available.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeView === "user" && userData && <UserStats data={userData} />}
|
||||
|
||||
{activeView === "linguistic" && linguisticData && (
|
||||
<LinguisticStats data={linguisticData} />
|
||||
)}
|
||||
|
||||
{activeView === "linguistic" && !linguisticData && (
|
||||
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
||||
No linguistic data available.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeView === "interactional" && interactionData && (
|
||||
<InteractionalStats data={interactionData} />
|
||||
)}
|
||||
|
||||
{activeView === "interactional" && !interactionData && (
|
||||
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
||||
No interactional data available.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeView === "cultural" && culturalData && (
|
||||
<CulturalStats data={culturalData} />
|
||||
)}
|
||||
|
||||
{activeView === "cultural" && !culturalData && (
|
||||
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
||||
No cultural data available.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activeView === "summary" && (
|
||||
<SummaryStats
|
||||
userData={userData}
|
||||
timeData={timeData}
|
||||
contentData={contentData}
|
||||
summary={summary}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeView === "emotional" && contentData && (
|
||||
<EmotionalStats contentData={contentData} />
|
||||
)}
|
||||
|
||||
{activeView === "emotional" && !contentData && (
|
||||
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
||||
No emotional data available.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeView === "user" && userData && (
|
||||
<UserStats data={userData} />
|
||||
)}
|
||||
|
||||
{activeView === "linguistic" && linguisticData && (
|
||||
<LinguisticStats data={linguisticData} />
|
||||
)}
|
||||
|
||||
{activeView === "linguistic" && !linguisticData && (
|
||||
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
||||
No linguistic data available.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeView === "interactional" && interactionData && (
|
||||
<InteractionalStats data={interactionData} />
|
||||
)}
|
||||
|
||||
{activeView === "interactional" && !interactionData && (
|
||||
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
||||
No interactional data available.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeView === "cultural" && culturalData && (
|
||||
<CulturalStats data={culturalData} />
|
||||
)}
|
||||
|
||||
{activeView === "cultural" && !culturalData && (
|
||||
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
||||
No cultural data available.
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default StatPage;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import StatsStyling from "../styles/stats_styling";
|
||||
|
||||
const styles = StatsStyling;
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL;
|
||||
|
||||
const UploadPage = () => {
|
||||
const [datasetName, setDatasetName] = useState("");
|
||||
@@ -40,16 +40,20 @@ const UploadPage = () => {
|
||||
setHasError(false);
|
||||
setReturnMessage("");
|
||||
|
||||
const response = await axios.post(`${API_BASE_URL}/datasets/upload`, formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
const response = await axios.post(
|
||||
`${API_BASE_URL}/datasets/upload`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const datasetId = Number(response.data.dataset_id);
|
||||
|
||||
setReturnMessage(
|
||||
`Upload queued successfully (dataset #${datasetId}). Redirecting to processing status...`
|
||||
`Upload queued successfully (dataset #${datasetId}). Redirecting to processing status...`,
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -58,7 +62,9 @@ const UploadPage = () => {
|
||||
} catch (error: unknown) {
|
||||
setHasError(true);
|
||||
if (axios.isAxiosError(error)) {
|
||||
const message = String(error.response?.data?.error || error.message || "Upload failed.");
|
||||
const message = String(
|
||||
error.response?.data?.error || error.message || "Upload failed.",
|
||||
);
|
||||
setReturnMessage(`Upload failed: ${message}`);
|
||||
} else {
|
||||
setReturnMessage("Upload failed due to an unexpected error.");
|
||||
@@ -75,12 +81,16 @@ const UploadPage = () => {
|
||||
<div>
|
||||
<h1 style={styles.sectionHeaderTitle}>Upload Dataset</h1>
|
||||
<p style={styles.sectionHeaderSubtitle}>
|
||||
Name your dataset, then upload posts and topic map files to generate analytics.
|
||||
Name your dataset, then upload posts and topic map files to
|
||||
generate analytics.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
style={{ ...styles.buttonPrimary, opacity: isSubmitting ? 0.75 : 1 }}
|
||||
style={{
|
||||
...styles.buttonPrimary,
|
||||
opacity: isSubmitting ? 0.75 : 1,
|
||||
}}
|
||||
onClick={uploadFiles}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
@@ -96,8 +106,12 @@ const UploadPage = () => {
|
||||
}}
|
||||
>
|
||||
<div style={{ ...styles.card, gridColumn: "auto" }}>
|
||||
<h2 style={{ ...styles.sectionTitle, color: "#24292f" }}>Dataset Name</h2>
|
||||
<p style={styles.sectionSubtitle}>Use a clear label so you can identify this upload later.</p>
|
||||
<h2 style={{ ...styles.sectionTitle, color: "#24292f" }}>
|
||||
Dataset Name
|
||||
</h2>
|
||||
<p style={styles.sectionSubtitle}>
|
||||
Use a clear label so you can identify this upload later.
|
||||
</p>
|
||||
<input
|
||||
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||
type="text"
|
||||
@@ -108,8 +122,12 @@ const UploadPage = () => {
|
||||
</div>
|
||||
|
||||
<div style={{ ...styles.card, gridColumn: "auto" }}>
|
||||
<h2 style={{ ...styles.sectionTitle, color: "#24292f" }}>Posts File (.jsonl)</h2>
|
||||
<p style={styles.sectionSubtitle}>Upload the raw post records export.</p>
|
||||
<h2 style={{ ...styles.sectionTitle, color: "#24292f" }}>
|
||||
Posts File (.jsonl)
|
||||
</h2>
|
||||
<p style={styles.sectionSubtitle}>
|
||||
Upload the raw post records export.
|
||||
</p>
|
||||
<input
|
||||
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||
type="file"
|
||||
@@ -122,16 +140,24 @@ const UploadPage = () => {
|
||||
</div>
|
||||
|
||||
<div style={{ ...styles.card, gridColumn: "auto" }}>
|
||||
<h2 style={{ ...styles.sectionTitle, color: "#24292f" }}>Topics File (.json)</h2>
|
||||
<p style={styles.sectionSubtitle}>Upload your topic bucket mapping file.</p>
|
||||
<h2 style={{ ...styles.sectionTitle, color: "#24292f" }}>
|
||||
Topics File (.json)
|
||||
</h2>
|
||||
<p style={styles.sectionSubtitle}>
|
||||
Upload your topic bucket mapping file.
|
||||
</p>
|
||||
<input
|
||||
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={(event) => setTopicBucketFile(event.target.files?.[0] ?? null)}
|
||||
onChange={(event) =>
|
||||
setTopicBucketFile(event.target.files?.[0] ?? null)
|
||||
}
|
||||
/>
|
||||
<p style={styles.subtleBodyText}>
|
||||
{topicBucketFile ? `Selected: ${topicBucketFile.name}` : "No file selected"}
|
||||
{topicBucketFile
|
||||
? `Selected: ${topicBucketFile.name}`
|
||||
: "No file selected"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,7 +169,8 @@ const UploadPage = () => {
|
||||
...(hasError ? styles.alertCardError : styles.alertCardInfo),
|
||||
}}
|
||||
>
|
||||
{returnMessage || "After upload, your dataset is queued for processing and you'll land on stats."}
|
||||
{returnMessage ||
|
||||
"After upload, your dataset is queued for processing and you'll land on stats."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user