Compare commits
6 Commits
c1a0324a03
...
b6de100a17
| Author | SHA1 | Date | |
|---|---|---|---|
| b6de100a17 | |||
| 5310568631 | |||
| 4b33f17b4b | |||
| 64783e764d | |||
| 8ac5207a11 | |||
| 090a57f4dd |
@@ -56,5 +56,17 @@ services:
|
|||||||
count: 1
|
count: 1
|
||||||
capabilities: [gpu]
|
capabilities: [gpu]
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
container_name: crosspost_frontend
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/app
|
||||||
|
- /app/node_modules
|
||||||
|
ports:
|
||||||
|
- "5173:5173"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
model_cache:
|
model_cache:
|
||||||
13
frontend/Dockerfile
Normal file
13
frontend/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy rest of the app
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 5173
|
||||||
|
|
||||||
|
CMD ["npm", "run", "dev", "--", "--host"]
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
import { Routes, Route } from "react-router-dom";
|
import { Navigate, Route, Routes } from "react-router-dom";
|
||||||
|
import AppLayout from "./components/AppLayout";
|
||||||
|
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";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route element={<AppLayout />}>
|
||||||
|
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||||
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/upload" element={<UploadPage />} />
|
<Route path="/upload" element={<UploadPage />} />
|
||||||
<Route path="/stats" element={<StatPage />} />
|
<Route path="/stats" element={<StatPage />} />
|
||||||
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
125
frontend/src/components/AppLayout.tsx
Normal file
125
frontend/src/components/AppLayout.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import StatsStyling from "../styles/stats_styling";
|
||||||
|
|
||||||
|
type ProfileResponse = {
|
||||||
|
user?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StatsStyling;
|
||||||
|
const API_BASE_URL = "http://localhost:5000";
|
||||||
|
|
||||||
|
const getUserLabel = (user: Record<string, unknown> | null) => {
|
||||||
|
if (!user) {
|
||||||
|
return "Signed in";
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = user.username;
|
||||||
|
if (typeof username === "string" && username.length > 0) {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = user.email;
|
||||||
|
if (typeof email === "string" && email.length > 0) {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Signed in";
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppLayout = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isSignedIn, setIsSignedIn] = useState(false);
|
||||||
|
const [currentUser, setCurrentUser] = useState<Record<string, unknown> | null>(null);
|
||||||
|
|
||||||
|
const syncAuthState = useCallback(async () => {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
setIsSignedIn(false);
|
||||||
|
setCurrentUser(null);
|
||||||
|
delete axios.defaults.headers.common.Authorization;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get<ProfileResponse>(`${API_BASE_URL}/profile`);
|
||||||
|
setIsSignedIn(true);
|
||||||
|
setCurrentUser(response.data.user ?? null);
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem("access_token");
|
||||||
|
delete axios.defaults.headers.common.Authorization;
|
||||||
|
setIsSignedIn(false);
|
||||||
|
setCurrentUser(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void syncAuthState();
|
||||||
|
}, [location.pathname, syncAuthState]);
|
||||||
|
|
||||||
|
const onAuthButtonClick = () => {
|
||||||
|
if (isSignedIn) {
|
||||||
|
localStorage.removeItem("access_token");
|
||||||
|
delete axios.defaults.headers.common.Authorization;
|
||||||
|
setIsSignedIn(false);
|
||||||
|
setCurrentUser(null);
|
||||||
|
navigate("/login", { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate("/login");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minHeight: "100vh",
|
||||||
|
background: "#f6f7fb",
|
||||||
|
fontFamily: styles.page.fontFamily,
|
||||||
|
color: "#111827",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ ...styles.container, padding: "16px 24px 0" }}>
|
||||||
|
<div style={{ ...styles.card, ...styles.headerBar }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
|
||||||
|
<span style={{ margin: 0, color: "#111827", fontSize: 18, fontWeight: 700 }}>
|
||||||
|
Ethnograph View
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
padding: "4px 10px",
|
||||||
|
borderRadius: 999,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontFamily: styles.page.fontFamily,
|
||||||
|
background: isSignedIn ? "#dcfce7" : "#fee2e2",
|
||||||
|
color: isSignedIn ? "#166534" : "#991b1b",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSignedIn ? `Signed in: ${getUserLabel(currentUser)}` : "Not signed in"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ ...styles.controls, flexWrap: "wrap" }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={isSignedIn ? styles.buttonSecondary : styles.buttonPrimary}
|
||||||
|
onClick={onAuthButtonClick}
|
||||||
|
>
|
||||||
|
{isSignedIn ? "Sign out" : "Sign in"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -1,68 +1,17 @@
|
|||||||
:root {
|
:root {
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
html,
|
||||||
font-weight: 500;
|
body,
|
||||||
color: #646cff;
|
#root {
|
||||||
text-decoration: inherit;
|
width: 100%;
|
||||||
}
|
height: 100%;
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
211
frontend/src/pages/Login.tsx
Normal file
211
frontend/src/pages/Login.tsx
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import StatsStyling from "../styles/stats_styling";
|
||||||
|
|
||||||
|
const styles = StatsStyling;
|
||||||
|
const API_BASE_URL = "http://localhost:5000";
|
||||||
|
const controlStyle = {
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: "100%",
|
||||||
|
boxSizing: "border-box" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [isRegisterMode, setIsRegisterMode] = useState(false);
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [info, setInfo] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||||
|
axios
|
||||||
|
.get(`${API_BASE_URL}/profile`)
|
||||||
|
.then(() => {
|
||||||
|
navigate("/upload", { replace: true });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
localStorage.removeItem("access_token");
|
||||||
|
delete axios.defaults.headers.common.Authorization;
|
||||||
|
});
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setError("");
|
||||||
|
setInfo("");
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isRegisterMode) {
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const token = response.data.access_token;
|
||||||
|
localStorage.setItem("access_token", token);
|
||||||
|
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||||
|
navigate("/upload");
|
||||||
|
}
|
||||||
|
} catch (requestError: unknown) {
|
||||||
|
if (axios.isAxiosError(requestError)) {
|
||||||
|
setError(
|
||||||
|
String(requestError.response?.data?.error || requestError.message || "Request failed")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setError("Unexpected error occurred.");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ ...styles.container, maxWidth: 560, padding: "48px 24px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...styles.card,
|
||||||
|
padding: 28,
|
||||||
|
background:
|
||||||
|
"linear-gradient(180deg, rgba(255,255,255,1) 0%, rgba(248,250,255,1) 100%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: 22, textAlign: "center" }}>
|
||||||
|
<h1 style={{ margin: 0, color: "#111827", fontSize: 30, lineHeight: 1.1 }}>
|
||||||
|
{isRegisterMode ? "Create your account" : "Welcome back"}
|
||||||
|
</h1>
|
||||||
|
<p style={{ margin: "8px 0 0", color: "#6b7280", fontSize: 14 }}>
|
||||||
|
{isRegisterMode
|
||||||
|
? "Register to start uploading and exploring your dataset insights."
|
||||||
|
: "Sign in to continue to your analytics workspace."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
style={{ display: "grid", gap: 12, maxWidth: 380, margin: "0 auto" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
style={{ ...styles.input, ...controlStyle }}
|
||||||
|
value={username}
|
||||||
|
onChange={(event) => setUsername(event.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isRegisterMode && (
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
style={{ ...styles.input, ...controlStyle }}
|
||||||
|
value={email}
|
||||||
|
onChange={(event) => setEmail(event.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
style={{ ...styles.input, ...controlStyle }}
|
||||||
|
value={password}
|
||||||
|
onChange={(event) => setPassword(event.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
style={{ ...styles.buttonPrimary, ...controlStyle, marginTop: 2 }}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading
|
||||||
|
? "Please wait..."
|
||||||
|
: isRegisterMode
|
||||||
|
? "Create account"
|
||||||
|
: "Sign in"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
color: "#b91c1c",
|
||||||
|
margin: "12px auto 0",
|
||||||
|
fontSize: 14,
|
||||||
|
maxWidth: 380,
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{info && (
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
color: "#166534",
|
||||||
|
margin: "12px auto 0",
|
||||||
|
fontSize: 14,
|
||||||
|
maxWidth: 380,
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{info}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 8,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ color: "#6b7280", fontSize: 14 }}>
|
||||||
|
{isRegisterMode ? "Already have an account?" : "New here?"}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={{
|
||||||
|
border: "none",
|
||||||
|
background: "transparent",
|
||||||
|
color: "#2563eb",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setError("");
|
||||||
|
setInfo("");
|
||||||
|
setIsRegisterMode((value) => !value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isRegisterMode ? "Switch to sign in" : "Create account"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
@@ -1,56 +1,132 @@
|
|||||||
import axios from 'axios'
|
import axios from "axios";
|
||||||
import './../App.css'
|
import { useState } from "react";
|
||||||
import { useState } from 'react'
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import StatsStyling from "../styles/stats_styling";
|
import StatsStyling from "../styles/stats_styling";
|
||||||
|
|
||||||
const styles = StatsStyling;
|
const styles = StatsStyling;
|
||||||
|
const API_BASE_URL = "http://localhost:5000";
|
||||||
|
|
||||||
const UploadPage = () => {
|
const UploadPage = () => {
|
||||||
let postFile: File | undefined;
|
const [postFile, setPostFile] = useState<File | null>(null);
|
||||||
let topicBucketFile: File | undefined;
|
const [topicBucketFile, setTopicBucketFile] = useState<File | null>(null);
|
||||||
const [returnMessage, setReturnMessage] = useState('')
|
const [returnMessage, setReturnMessage] = useState("");
|
||||||
const navigate = useNavigate()
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [hasError, setHasError] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const uploadFiles = async () => {
|
const uploadFiles = async () => {
|
||||||
if (!postFile || !topicBucketFile) {
|
if (!postFile || !topicBucketFile) {
|
||||||
alert('Please upload all files before uploading.')
|
setHasError(true);
|
||||||
return
|
setReturnMessage("Please upload both files before continuing.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData()
|
const formData = new FormData();
|
||||||
formData.append('posts', postFile)
|
formData.append("posts", postFile);
|
||||||
formData.append('topics', topicBucketFile)
|
formData.append("topics", topicBucketFile);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('http://localhost:5000/upload', formData, {
|
setIsSubmitting(true);
|
||||||
headers: {
|
setHasError(false);
|
||||||
'Content-Type': 'multipart/form-data',
|
setReturnMessage("");
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log('Files uploaded successfully:', response.data)
|
|
||||||
setReturnMessage(`Upload successful! Posts: ${response.data.posts_count}, Comments: ${response.data.comments_count}`)
|
|
||||||
navigate('/stats')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error uploading files:', error)
|
|
||||||
setReturnMessage('Error uploading files. Error details: ' + error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div style={{...styles.container, ...styles.grid, margin: "0"}}>
|
|
||||||
<div style={{ ...styles.card }}>
|
|
||||||
<h2 style={{color: "black" }}>Posts File</h2>
|
|
||||||
<input style={{color: "black" }} type="file" onChange={(e) => postFile = e.target.files?.[0]}></input>
|
|
||||||
</div>
|
|
||||||
<div style={{ ...styles.card }}>
|
|
||||||
<h2 style={{color: "black" }}>Topic Buckets File</h2>
|
|
||||||
<input style={{color: "black" }} type="file" onChange={(e) => topicBucketFile = e.target.files?.[0]}></input>
|
|
||||||
</div>
|
|
||||||
<button onClick={uploadFiles}>Upload</button>
|
|
||||||
|
|
||||||
<p>{returnMessage}</p>
|
const response = await axios.post(`${API_BASE_URL}/upload`, formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setReturnMessage(
|
||||||
|
`Upload queued successfully (dataset #${response.data.dataset_id}). Redirecting to insights...`
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate("/stats");
|
||||||
|
}, 400);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setHasError(true);
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const message = String(error.response?.data?.error || error.message || "Upload failed.");
|
||||||
|
setReturnMessage(`Upload failed: ${message}`);
|
||||||
|
} else {
|
||||||
|
setReturnMessage("Upload failed due to an unexpected error.");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.page}>
|
||||||
|
<div style={{ ...styles.container, maxWidth: 1100 }}>
|
||||||
|
<div style={{ ...styles.card, ...styles.headerBar }}>
|
||||||
|
<div>
|
||||||
|
<h1 style={{ margin: 0, color: "#111827", fontSize: 28 }}>Upload Dataset</h1>
|
||||||
|
<p style={{ margin: "8px 0 0", color: "#6b7280", fontSize: 14 }}>
|
||||||
|
Add your posts and topic map files to generate fresh analytics.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
<button
|
||||||
}
|
type="button"
|
||||||
|
style={{ ...styles.buttonPrimary, opacity: isSubmitting ? 0.75 : 1 }}
|
||||||
|
onClick={uploadFiles}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Uploading..." : "Upload and Analyze"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...styles.grid,
|
||||||
|
marginTop: 14,
|
||||||
|
gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ ...styles.card, gridColumn: "auto" }}>
|
||||||
|
<h2 style={{ ...styles.sectionTitle, color: "#111827" }}>Posts File (.jsonl)</h2>
|
||||||
|
<p style={styles.sectionSubtitle}>Upload the raw post records export.</p>
|
||||||
|
<input
|
||||||
|
style={{ ...styles.input, width: "80%", maxWidth: "100%" }}
|
||||||
|
type="file"
|
||||||
|
accept=".jsonl"
|
||||||
|
onChange={(event) => setPostFile(event.target.files?.[0] ?? null)}
|
||||||
|
/>
|
||||||
|
<p style={{ margin: "10px 0 0", fontSize: 13, color: "#374151" }}>
|
||||||
|
{postFile ? `Selected: ${postFile.name}` : "No file selected"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ ...styles.card, gridColumn: "auto" }}>
|
||||||
|
<h2 style={{ ...styles.sectionTitle, color: "#111827" }}>Topics File (.json)</h2>
|
||||||
|
<p style={styles.sectionSubtitle}>Upload your topic bucket mapping file.</p>
|
||||||
|
<input
|
||||||
|
style={{ ...styles.input, width: "80%", maxWidth: "100%" }}
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
onChange={(event) => setTopicBucketFile(event.target.files?.[0] ?? null)}
|
||||||
|
/>
|
||||||
|
<p style={{ margin: "10px 0 0", fontSize: 13, color: "#374151" }}>
|
||||||
|
{topicBucketFile ? `Selected: ${topicBucketFile.name}` : "No file selected"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...styles.card,
|
||||||
|
marginTop: 14,
|
||||||
|
borderColor: hasError ? "rgba(185, 28, 28, 0.28)" : "rgba(0,0,0,0.06)",
|
||||||
|
background: hasError ? "#fff5f5" : "#ffffff",
|
||||||
|
color: hasError ? "#991b1b" : "#374151",
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{returnMessage || "After upload, your dataset is queued for processing and you'll land on stats."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default UploadPage;
|
export default UploadPage;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from flask_jwt_extended import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from server.analysis.stat_gen import StatGen
|
from server.analysis.stat_gen import StatGen
|
||||||
from server.analysis.enrichment import DatasetEnrichment
|
|
||||||
from server.exceptions import NotAuthorisedException, NonExistentDatasetException
|
from server.exceptions import NotAuthorisedException, NonExistentDatasetException
|
||||||
from server.db.database import PostgresConnector
|
from server.db.database import PostgresConnector
|
||||||
from server.core.auth import AuthManager
|
from server.core.auth import AuthManager
|
||||||
|
|||||||
Reference in New Issue
Block a user