Make hice home page. Remake details and config pannel to be nicer for small (time still to come)
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
html {
|
||||
color-scheme: light dark;
|
||||
font-family: system-ui;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user