Make hice home page. Remake details and config pannel to be nicer for small (time still to come)

This commit is contained in:
William Jeynes
2026-04-28 21:50:13 +01:00
parent 0491a55bf6
commit 477cbf6b32
17 changed files with 695 additions and 100 deletions
-8
View File
@@ -1,8 +0,0 @@
html {
color-scheme: light dark;
font-family: system-ui;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
-30
View File
@@ -1,30 +0,0 @@
import { useEffect, useState } from "react";
import { VizSmallConnected } from "./VizSmallConnected";
import { VizTimeFilter } from "./VizTimeFilter";
function Home() {
return (
<div>
<h1>Will Jeynes - LLMs for Disinformation Analysis - Graph Visualisations</h1>
<p><a href="#small">Default</a></p>
<p><a href="#time">Time-Filter</a></p>
</div>
);
}
export function AppRouter() {
const [route, setRoute] = useState(() => window.location.hash);
useEffect(() => {
const onHashChange = () => {
setRoute(window.location.hash);
};
window.addEventListener("hashchange", onHashChange);
return () => window.removeEventListener("hashchange", onHashChange);
}, []);
console.log(route)
if (route === "#small") return <VizSmallConnected />;
if (route === "#time") return <VizTimeFilter />;
return <Home />;
}
+86
View File
@@ -0,0 +1,86 @@
export function Home() {
return (
<div className="flex justify-center">
<div className="w-full">
<div className="m-3 bg-gray-200 rounded-3xl p-3">
<h1 className="text-3xl text-center">LLMs for Disinformation Analysis</h1>
<h2 className="text-3xl text-center">Dataset Visualizations</h2>
<a href="https://jeynes.uk">
<h2 className="underline text-center">By Will Jeynes</h2>
</a>
</div>
<div className="flex flex-wrap justify-between">
<div className="m-2 rounded-3xl w-full sm:w-[48%] bg-gray-200 p-10 flex flex-col items-center">
<a href="#small">
<h3 className="text-2xl text-center underline">Default View</h3>
</a>
<a href="#small" className="m-5">
<img src="small.png" className="border-2 h-64" />
</a>
<p className="text-center">
A filtered collection of the whole dataset, containing only reasonably sized components
</p>
<p className="text-center">
A great introduction to the dataset on a curated set of examples
</p>
</div>
<div className="m-2 rounded-3xl w-full sm:w-[48%] bg-gray-200 p-10 flex flex-col items-center">
<a href="#time">
<h3 className="text-2xl text-center underline">Time Filtered</h3>
</a>
<a href="#time" className="m-5">
<img src="time.png" className="border-2 h-64" />
</a>
<p className="text-center">
A visualisation showing only the largest component, normally too large to be understandable
</p>
<p className="text-center">
Configurable, scrubber date filter allows migration to be seen over time
</p>
</div>
</div>
<div className="m-3 bg-gray-200 rounded-3xl p-3">
<h3 className="text-xl font-semibold">
Project Description
</h3>
<p className="text-lg">
Description coming soon
</p>
<h3 className="text-xl font-semibold mt-3">
Sources
</h3>
<div className="flex flex-wrap">
<a
href="https://huggingface.co/datasets/WillJeynes/LLMsForDisinformationAnalysis-Dataset"
className="block bg-white rounded-xl px-4 py-2 underline text-center w-64 m-1"
>
Dataset (Hugging Face)
</a>
<a
href="https://github.com/WillJeynes/LLMsForDisinformationAnalysis/"
className="block bg-white rounded-xl px-4 py-2 underline text-center w-64 m-1"
>
Dataset GitHub
</a>
<a
href="https://github.com/WillJeynes/LLMsForDisinformationPrediction/"
className="block bg-white rounded-xl px-4 py-2 underline text-center w-64 m-1"
>
Project Source Code
</a>
</div>
</div>
</div>
</div>
);
}
+19 -42
View File
@@ -4,6 +4,10 @@ import * as d3 from "d3-force-3d";
import data from "./data.json";
import titlesData from "./titles.json";
import HomeButton from "./utils/HomeButton";
import { FloatingPanel } from "./utils/FloatingPanel";
import { DetailsPanel } from "./utils/DetailsPanel";
import { FloatingPanelStack } from "./utils/FloatingPanelStack";
function drawRoundedRect(ctx, x, y, width, height, radius) {
const r = Math.min(radius, width / 2, height / 2);
@@ -33,6 +37,7 @@ function buildGraph(data) {
id: cluster.cluster_id,
label: titleMap.get(cluster.cluster_id) || cluster.title || "Unnamed Claim Cluster",
type: "claim_cluster",
type_nice: "Claim",
members: cluster.members
});
});
@@ -42,6 +47,7 @@ function buildGraph(data) {
id: cluster.cluster_id,
label: titleMap.get(cluster.cluster_id) || cluster.title || "Unnamed Event Cluster",
type: "event_cluster",
type_nice: "Event",
members: cluster.members
});
});
@@ -131,7 +137,7 @@ export function VizSmallConnected() {
d3.forceManyBody().strength(-10000)
);
// Link distance
fgRef.current.d3Force(
@@ -151,7 +157,7 @@ export function VizSmallConnected() {
fgRef.current.d3ReheatSimulation();
}, [graphData]);
useEffect(() => {
useEffect(() => {
if (fgRef.current) {
fgRef.current.zoom(0.01, 0);
}
@@ -159,6 +165,7 @@ export function VizSmallConnected() {
return (
<div>
<HomeButton />
<ForceGraph2D
ref={fgRef}
graphData={graphData}
@@ -217,20 +224,13 @@ export function VizSmallConnected() {
ctx.fill();
}}
/>
<div
style={{
position: "absolute",
top: "10px",
right: "10px",
borderRadius: "3px",
backgroundColor: "gray",
padding: "20px",
maxWidth: "500px"
}}
>
<p><a href="#home">Go Home</a></p>
<h2>Config</h2>
<FloatingPanelStack>
<DetailsPanel selectedNode={selectedNode} data={data} />
<FloatingPanel title={"Key"}>
<p>Trigger Event Cluster</p>
<p>Claim Cluster</p>
</FloatingPanel>
<FloatingPanel title={"Config"}>
<label>
Min connected graph size: <strong>{minGraphSize}</strong>
</label>
@@ -242,35 +242,12 @@ export function VizSmallConnected() {
value={minGraphSize}
onChange={(e) => setMinGraphSize(Number(e.target.value))}
/>
</FloatingPanel>
</FloatingPanelStack>
<h2>Details</h2>
{selectedNode ? (
<div>
<p><strong>Title:</strong> {selectedNode.label}</p>
{selectedNode.members && (
<div>
<p><strong>Members:</strong></p>
<ul>
{selectedNode.members.map((m) => {
const memberData =
data.claims.find((c) => c.id === m) ||
data.events.find((e) => e.id === m);
return (
<li key={m}>
{memberData ? memberData.text : m}
</li>
);
})}
</ul>
</div>
)}
</div>
) : (
<p>Click a node to see details</p>
)}
</div>
</div>
);
}
+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";
+1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="./index.css" type="text/css" rel="stylesheet" />
<title>Parcel React App</title>
</head>
<body>
+21 -1
View File
@@ -1,6 +1,26 @@
import { createRoot } from "react-dom/client";
import { StrictMode } from "react";
import { AppRouter } from "./AppRouter";
import { useEffect, useState } from "react";
import { VizSmallConnected } from "./VizSmallConnected";
import { VizTimeFilter } from "./VizTimeFilter";
import { Home } from "./Home";
export function AppRouter() {
const [route, setRoute] = useState(() => window.location.hash);
useEffect(() => {
const onHashChange = () => {
setRoute(window.location.hash);
};
window.addEventListener("hashchange", onHashChange);
return () => window.removeEventListener("hashchange", onHashChange);
}, []);
if (route === "#small") return <VizSmallConnected />;
if (route === "#time") return <VizTimeFilter />;
return <Home />;
}
let container = document.getElementById("app")!;
let root = createRoot(container);
@@ -0,0 +1,43 @@
import { FloatingPanel } from "./FloatingPanel";
export function DetailsPanel({ selectedNode, data }) {
return (
<FloatingPanel title="Details">
{selectedNode ? (
<div className="space-y-3">
<p>
<strong>Type:</strong> {selectedNode.type_nice} Cluster
</p>
<p>
<strong>Title:</strong> {selectedNode.label}
</p>
{selectedNode.members && (
<div>
<p className="font-semibold">Members:</p>
<ul className="list-disc list-inside text-sm">
{selectedNode.members.map((m) => {
const memberData =
data.claims.find((c) => c.id === m) ||
data.events.find((e) => e.id === m);
return (
<li key={m}>
{memberData ? memberData.text : m}
</li>
);
})}
</ul>
</div>
)}
</div>
) : (
<p className="text-sm text-gray-500">
Click a node to see details
</p>
)}
</FloatingPanel>
);
}
@@ -0,0 +1,37 @@
import { useState } from "react";
export function FloatingPanel({
title,
children,
defaultOpen = true,
}) {
const [open, setOpen] = useState(defaultOpen);
return (
<div className="bg-white shadow-lg rounded-2xl w-80 overflow-hidden transition-all">
<button
onClick={() => setOpen(!open)}
className="w-full flex items-center justify-between px-4 py-2 bg-gray-100 hover:bg-gray-200 transition"
>
<span className="font-semibold">{title}</span>
<span
className={`transform transition-transform ${
open ? "rotate-180" : ""
}`}
>
</span>
</button>
<div
className={`overflow-scroll transition-all duration-300 ${
open ? "max-h-[50vh] p-4" : "max-h-0 p-0"
}`}
>
{children}
</div>
</div>
);
}
@@ -0,0 +1,7 @@
export function FloatingPanelStack({ children }) {
return (
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 items-end">
{children}
</div>
);
}
@@ -0,0 +1,23 @@
export default function HomeButton() {
return (
<a
href="#home"
className="fixed top-5 left-5 z-50 bg-black shadow-lg rounded-full p-3 hover:bg-gray-800 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-gray-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 10.5L12 3l9 7.5M5 9.75V21h14V9.75"
/>
</svg>
</a>
);
}