/** * @license * SPDX-License-Identifier: Apache-2.0 */ import React, { useState, useMemo, useRef } from 'react'; import { parseSocialData, ParseResult, getCoreMembers, MemberMetrics, findShortestPath, UserNode, detectClusters, ClusterInfo } from './lib/parser'; import NetworkGraph from './components/NetworkGraph'; type Tab = 'analysis' | 'clusters' | 'filters' | 'logs'; export default function App() { const [rawData, setRawData] = useState(''); const [analysis, setAnalysis] = useState(null); const [selectedUser, setSelectedUser] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [minDegree, setMinDegree] = useState(2); const [pathTargetQuery, setPathTargetQuery] = useState(''); const [highlightedPath, setHighlightedPath] = useState(null); const [ignoredUsers, setIgnoredUsers] = useState(['carterpcs', 'clavicular']); const [newIgnoreUser, setNewIgnoreUser] = useState(''); const [linkFilter, setLinkFilter] = useState<'all' | 'following' | 'follower'>('all'); const [activeTab, setActiveTab] = useState('analysis'); const fileInputRef = useRef(null); const handleAnalyze = () => { const data = parseSocialData(rawData); setAnalysis(data); setSelectedUser(null); setHighlightedPath(null); }; const handleImport = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { const content = event.target?.result as string; if (file.name.endsWith('.json')) { try { const data = JSON.parse(content); // Simple validation and reconstruction of Map objects if (data.nodes && data.relationships) { const allUsers = new Map(); data.nodes.forEach((n: any) => allUsers.set(n.username, n)); const relationships = new Map(); Object.entries(data.relationships).forEach(([key, val]: [string, any]) => { relationships.set(key, { following: val.following.map((u: string) => allUsers.get(u) || { username: u, displayName: u }), followers: val.followers.map((u: string) => allUsers.get(u) || { username: u, displayName: u }) }); }); setAnalysis({ allUsers, relationships, logs: [`Imported graph from ${file.name}`] }); } } catch (err) { alert("Failed to parse JSON import."); } } else { setRawData(content); handleAnalyze(); } }; reader.readAsText(file); }; const exportJSON = () => { if (!analysis) return; const exportData = { nodes: Array.from(analysis.allUsers.values()), relationships: Object.fromEntries( Array.from(analysis.relationships.entries()).map(([k, v]) => [ k, { following: v.following.map(x => x.username), followers: v.followers.map(x => x.username) } ]) ) }; const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'network_graph.json'; a.click(); URL.revokeObjectURL(url); }; const exportCSV = () => { if (!analysis) return; let csv = "source,target,type\n"; analysis.relationships.forEach((rels, subject) => { rels.following.forEach(u => csv += `${subject},${u.username},following\n`); rels.followers.forEach(u => csv += `${u.username},${subject},follower\n`); }); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'network_edges.csv'; a.click(); URL.revokeObjectURL(url); }; const handleFindPath = (target: string) => { if (!analysis || !selectedUser) return; const path = findShortestPath(analysis, selectedUser.username, target); if (path) { setHighlightedPath(path); setPathTargetQuery(''); } else { alert("No path found between these nodes."); } }; const filteredAnalysis = useMemo(() => { if (!analysis) return null; const ignoredSet = new Set(ignoredUsers.map(u => u.toLowerCase())); // Deep clone basic structure const newAllUsers = new Map(); analysis.allUsers.forEach((node, username) => { if (!ignoredSet.has(username.toLowerCase())) { newAllUsers.set(username, node); } }); const newRelationships = new Map(); analysis.relationships.forEach((rels, subject) => { if (ignoredSet.has(subject.toLowerCase())) return; const filteredFollowing = rels.following.filter(u => !ignoredSet.has(u.username.toLowerCase())); const filteredFollowers = rels.followers.filter(u => !ignoredSet.has(u.username.toLowerCase())); newRelationships.set(subject, { following: filteredFollowing, followers: filteredFollowers }); }); return { allUsers: newAllUsers, relationships: newRelationships, logs: analysis.logs }; }, [analysis, ignoredUsers]); const allMembers = useMemo(() => filteredAnalysis ? getCoreMembers(filteredAnalysis) : [], [filteredAnalysis]); const metricsMap = useMemo(() => new Map(allMembers.map(m => [m.username, m])), [allMembers]); const clusters = useMemo(() => filteredAnalysis ? detectClusters(filteredAnalysis, allMembers) : [], [filteredAnalysis, allMembers]); const coreMembers = useMemo(() => allMembers.slice(0, 10), [allMembers]); const altCandidates = useMemo(() => allMembers.filter(m => m.isAltCandidate), [allMembers]); const displayedMembers = useMemo(() => { if (!filteredAnalysis) return []; if (searchQuery.trim() === '') return coreMembers; const lowerQ = searchQuery.toLowerCase(); return allMembers.filter(m => m.username.toLowerCase().includes(lowerQ)).slice(0, 20); }, [allMembers, coreMembers, searchQuery, filteredAnalysis]); const handleNodeClick = (username: string) => { if (!filteredAnalysis) return; const member = allMembers.find(m => m.username === username); if (member) { setSelectedUser(member); setHighlightedPath(null); // Reset path when a new base user is selected } }; const addIgnoredUser = (username: string) => { const trimmed = username.trim().replace(/^@/, ''); if (trimmed && !ignoredUsers.includes(trimmed)) { setIgnoredUsers([...ignoredUsers, trimmed]); } setNewIgnoreUser(''); }; const removeIgnoredUser = (username: string) => { setIgnoredUsers(ignoredUsers.filter(u => u !== username)); }; return (
{/* Header Navigation */}
{/* Sidebar: Parsing & Data Source */}