From 24277e01047328cb5224073aaceb4f0a9660ce59 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Wed, 4 Mar 2026 20:09:55 +0000 Subject: [PATCH 1/3] fix(frontend): move loading card higher up Looks weird lower down on the screen --- frontend/src/pages/Stats.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/Stats.tsx b/frontend/src/pages/Stats.tsx index 586d6c2..a290c7f 100644 --- a/frontend/src/pages/Stats.tsx +++ b/frontend/src/pages/Stats.tsx @@ -139,7 +139,7 @@ const StatPage = () => { if (loading) { return (
-
+
From 5fc1f1532f9f5e86f141573d63e31bb0e7e7aecf Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Wed, 4 Mar 2026 20:20:34 +0000 Subject: [PATCH 2/3] feat(user stats): updated styling and stats in user page Interaction graph was taking up too much space and was the only thing on the screen. Further statistics were added however these may be removed in favour of more informative statistics --- frontend/src/components/UserStats.tsx | 115 ++++++++++++++++++++++---- frontend/src/pages/Stats.tsx | 14 ++-- 2 files changed, 108 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/UserStats.tsx b/frontend/src/components/UserStats.tsx index b33fcd4..bb060cc 100644 --- a/frontend/src/components/UserStats.tsx +++ b/frontend/src/components/UserStats.tsx @@ -1,3 +1,4 @@ +import { useEffect, useMemo, useRef, useState } from "react"; import ForceGraph3D from "react-force-graph-3d"; import { @@ -6,12 +7,19 @@ import { } from '../types/ApiTypes'; import StatsStyling from "../styles/stats_styling"; +import Card from "./Card"; const styles = StatsStyling; +type GraphLink = { + source: string; + target: string; + value: number; +}; + function ApiToGraphData(apiData: InteractionGraph) { const nodes = Object.keys(apiData).map(username => ({ id: username })); - const links = []; + const links: GraphLink[] = []; for (const [source, targets] of Object.entries(apiData)) { for (const [target, count] of Object.entries(targets)) { @@ -35,27 +43,106 @@ function ApiToGraphData(apiData: InteractionGraph) { const UserStats = (props: { data: UserAnalysisResponse }) => { - const graphData = ApiToGraphData(props.data.interaction_graph); + const graphData = useMemo(() => ApiToGraphData(props.data.interaction_graph), [props.data.interaction_graph]); + const graphContainerRef = useRef(null); + const [graphSize, setGraphSize] = useState({ width: 720, height: 540 }); + + useEffect(() => { + const updateGraphSize = () => { + const containerWidth = graphContainerRef.current?.clientWidth ?? 720; + const nextWidth = Math.max(320, Math.floor(containerWidth)); + const nextHeight = nextWidth < 700 ? 300 : 540; + setGraphSize({ width: nextWidth, height: nextHeight }); + }; + + updateGraphSize(); + window.addEventListener("resize", updateGraphSize); + + return () => window.removeEventListener("resize", updateGraphSize); + }, []); + + const totalUsers = props.data.users.length; + const connectedUsers = graphData.nodes.length; + const totalInteractions = graphData.links.reduce((sum, link) => sum + link.value, 0); + const avgInteractionsPerConnectedUser = connectedUsers ? totalInteractions / connectedUsers : 0; + + const strongestLink = graphData.links.reduce((best, current) => { + if (!best || current.value > best.value) { + return current; + } + return best; + }, null); + + const highlyInteractiveUser = [...props.data.users].sort((a, b) => b.comment_share - a.comment_share)[0]; + + const mostActiveUser = props.data.top_users.find(u => u.author !== "[deleted]"); return (
-

User Interaction Graph

-

- This graph visualizes interactions between users based on comments and replies. - Nodes represent users, and edges represent interactions (e.g., comments or replies) between them. -

-
- + + + + + + ${strongestLink.target}` : "—"} + sublabel={strongestLink ? `${strongestLink.value.toLocaleString()} interactions` : "No graph edges after filtering"} + style={{ gridColumn: "span 6" }} + /> + + +
+

User Interaction Graph

+

+ Nodes represent users and links represent conversation interactions. +

+
+ Math.sqrt(link.value)} + linkDirectionalParticles={1} + linkDirectionalParticleSpeed={0.004} + linkWidth={(link) => Math.sqrt(Number(link.value))} nodeLabel={(node) => `${node.id}`} - /> + /> +
+
); } -export default UserStats; \ No newline at end of file +export default UserStats; diff --git a/frontend/src/pages/Stats.tsx b/frontend/src/pages/Stats.tsx index a290c7f..683584f 100644 --- a/frontend/src/pages/Stats.tsx +++ b/frontend/src/pages/Stats.tsx @@ -4,7 +4,7 @@ import { useParams } from "react-router-dom"; import StatsStyling from "../styles/stats_styling"; import SummaryStats from "../components/SummaryStats"; import EmotionalStats from "../components/EmotionalStats"; -import InteractionStats from "../components/UserStats"; +import UserStats from "../components/UserStats"; import { type SummaryResponse, @@ -20,7 +20,7 @@ const StatPage = () => { const { datasetId: routeDatasetId } = useParams<{ datasetId: string }>(); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); - const [activeView, setActiveView] = useState<"summary" | "emotional" | "interaction">("summary"); + const [activeView, setActiveView] = useState<"summary" | "emotional" | "user">("summary"); const [userData, setUserData] = useState(null); const [timeData, setTimeData] = useState(null); @@ -213,10 +213,10 @@ return (
@@ -239,8 +239,8 @@ return (
)} - {activeView === "interaction" && userData && ( - + {activeView === "user" && userData && ( + )}
From e20d0689e8b8bcecce8fdaff2ee7b3548bdbbdb2 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Wed, 4 Mar 2026 21:18:59 +0000 Subject: [PATCH 3/3] fix(celery): adjust try-catch logic to improve error handling Capturing the instantiation of the database and dataset manager objects inside the try-catch will cause errors if something else fails. If an exception occurs and the dataset_manager is not initialised, the code inside the catch block will fail. --- server/queue/tasks.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/server/queue/tasks.py b/server/queue/tasks.py index f2f3268..a089596 100644 --- a/server/queue/tasks.py +++ b/server/queue/tasks.py @@ -2,17 +2,15 @@ import pandas as pd from server.queue.celery_app import celery from server.analysis.enrichment import DatasetEnrichment +from server.db.database import PostgresConnector +from server.core.datasets import DatasetManager @celery.task(bind=True, max_retries=3) def process_dataset(self, dataset_id: int, posts: list, topics: dict): + db = PostgresConnector() + dataset_manager = DatasetManager(db) try: - from server.db.database import PostgresConnector - from server.core.datasets import DatasetManager - - db = PostgresConnector() - dataset_manager = DatasetManager(db) - df = pd.DataFrame(posts) processor = DatasetEnrichment(df, topics)