Finish off the links between frontend and backend #10

Merged
dylan merged 24 commits from feat/add-frontend-pages into main 2026-03-18 20:30:19 +00:00
4 changed files with 63 additions and 50 deletions
Showing only changes of commit 86926898ce - Show all commits

View File

@@ -40,6 +40,9 @@ const InteractionalStats = ({ data }: InteractionalStatsProps) => {
const singleCommentAuthorRatio = typeof concentration?.single_comment_author_ratio === "number"
? concentration.single_comment_author_ratio
: null;
const singleCommentAuthors = typeof concentration?.single_comment_authors === "number"
? concentration.single_comment_authors
: null;
const topPairs = (data.top_interaction_pairs ?? [])
.filter((item): item is [[string, string], number] => {
@@ -84,48 +87,55 @@ const InteractionalStats = ({ data }: InteractionalStatsProps) => {
return (
<div style={styles.page}>
<div style={{ ...styles.container, ...styles.grid }}>
<div style={{ ...styles.card, gridColumn: "span 12" }}>
<h2 style={styles.sectionTitle}>Conversation Overview</h2>
<p style={styles.sectionSubtitle}>Who talks to who, and how concentrated the replies are.</p>
</div>
<Card
label="Avg Thread Depth"
label="Average Reply Depth"
value={typeof data.average_thread_depth === "number" ? data.average_thread_depth.toFixed(2) : "—"}
sublabel="Depth from reply chains"
sublabel="How deep reply chains usually go"
style={{ gridColumn: "span 3" }}
/>
<Card
label="Network Users"
label="Users in Network"
value={userCount.toLocaleString()}
sublabel="Authors in interaction graph"
sublabel="Users in the reply graph"
style={{ gridColumn: "span 3" }}
/>
<Card
label="Unique Links"
label="User-to-User Links"
value={edgeCount.toLocaleString()}
sublabel="Directed source-target pairs"
sublabel="Unique reply directions"
style={{ gridColumn: "span 3" }}
/>
<Card
label="Interaction Volume"
label="Total Replies"
value={interactionVolume.toLocaleString()}
sublabel="Sum of link weights"
sublabel="All reply links combined"
style={{ gridColumn: "span 3" }}
/>
<Card
label="Top 10% Comment Share"
label="Concentrated Replies"
value={topTenSharePercent === null ? "-" : `${topTenSharePercent.toFixed(1)}%`}
sublabel={topTenAuthorCount === null || totalCommentingAuthors === null
? "Comment volume held by top commenters"
? "Reply share from the top 10% commenters"
: `${topTenAuthorCount.toLocaleString()} of ${totalCommentingAuthors.toLocaleString()} authors`}
style={{ gridColumn: "span 6" }}
/>
<Card
label="Single-Comment Authors"
value={singleCommentAuthorRatio === null ? "-" : `${(singleCommentAuthorRatio * 100).toFixed(1)}%`}
sublabel="Authors who commented exactly once"
sublabel={singleCommentAuthors === null
? "Authors who commented exactly once"
: `${singleCommentAuthors.toLocaleString()} authors commented exactly once`}
style={{ gridColumn: "span 6" }}
/>
<div style={{ ...styles.card, gridColumn: "span 12" }}>
<h2 style={styles.sectionTitle}>Interaction Visuals</h2>
<p style={styles.sectionSubtitle}>Quick charts for interaction direction and conversation concentration.</p>
<h2 style={styles.sectionTitle}>Conversation Visuals</h2>
<p style={styles.sectionSubtitle}>Main reply links and concentration split.</p>
<div style={{ ...styles.grid, marginTop: 12 }}>
<div style={{ ...styles.cardBase, gridColumn: "span 6" }}>
@@ -175,8 +185,8 @@ const InteractionalStats = ({ data }: InteractionalStatsProps) => {
</div>
<div style={{ ...styles.card, gridColumn: "span 12" }}>
<h2 style={styles.sectionTitle}>Top Interaction Pairs</h2>
<p style={styles.sectionSubtitle}>Most frequent directed reply paths between users.</p>
<h2 style={styles.sectionTitle}>Frequent Reply Paths</h2>
<p style={styles.sectionSubtitle}>Most common user-to-user reply paths.</p>
{!topPairs.length ? (
<div style={styles.topUserMeta}>No interaction pair data available.</div>
) : (

View File

@@ -21,28 +21,33 @@ const LinguisticStats = ({ data }: LinguisticStatsProps) => {
return (
<div style={styles.page}>
<div style={{ ...styles.container, ...styles.grid }}>
<div style={{ ...styles.card, gridColumn: "span 12" }}>
<h2 style={styles.sectionTitle}>Language Overview</h2>
<p style={styles.sectionSubtitle}>Quick read on how broad and repetitive the wording is.</p>
</div>
<Card
label="Total Tokens"
label="Total Words"
value={lexical?.total_tokens?.toLocaleString() ?? "—"}
sublabel="After token filtering"
sublabel="Words after basic filtering"
style={{ gridColumn: "span 4" }}
/>
<Card
label="Unique Tokens"
label="Unique Words"
value={lexical?.unique_tokens?.toLocaleString() ?? "—"}
sublabel="Distinct vocabulary items"
sublabel="Different words used"
style={{ gridColumn: "span 4" }}
/>
<Card
label="Type-Token Ratio"
label="Vocabulary Variety"
value={typeof lexical?.ttr === "number" ? lexical.ttr.toFixed(4) : "—"}
sublabel="Vocabulary richness proxy"
sublabel="Higher means less repetition"
style={{ gridColumn: "span 4" }}
/>
<div style={{ ...styles.card, gridColumn: "span 4" }}>
<h2 style={styles.sectionTitle}>Top Words</h2>
<p style={styles.sectionSubtitle}>Most frequent filtered terms.</p>
<p style={styles.sectionSubtitle}>Most used single words.</p>
<div style={{ ...styles.topUsersList, maxHeight: 360, overflowY: "auto" }}>
{topWords.map((item) => (
<div key={item.word} style={styles.topUserItem}>
@@ -55,7 +60,7 @@ const LinguisticStats = ({ data }: LinguisticStatsProps) => {
<div style={{ ...styles.card, gridColumn: "span 4" }}>
<h2 style={styles.sectionTitle}>Top Bigrams</h2>
<p style={styles.sectionSubtitle}>Most frequent 2-word phrases.</p>
<p style={styles.sectionSubtitle}>Most used 2-word phrases.</p>
<div style={{ ...styles.topUsersList, maxHeight: 360, overflowY: "auto" }}>
{topBigrams.map((item) => (
<div key={item.ngram} style={styles.topUserItem}>
@@ -68,7 +73,7 @@ const LinguisticStats = ({ data }: LinguisticStatsProps) => {
<div style={{ ...styles.card, gridColumn: "span 4" }}>
<h2 style={styles.sectionTitle}>Top Trigrams</h2>
<p style={styles.sectionSubtitle}>Most frequent 3-word phrases.</p>
<p style={styles.sectionSubtitle}>Most used 3-word phrases.</p>
<div style={{ ...styles.topUsersList, maxHeight: 360, overflowY: "auto" }}>
{topTrigrams.map((item) => (
<div key={item.ngram} style={styles.topUserItem}>

View File

@@ -58,15 +58,13 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
const [selectedUser, setSelectedUser] = useState<string | null>(null);
const selectedUserData: User | null = userData?.users.find((u) => u.author === selectedUser) ?? null;
console.log(summary)
return (
<div style={styles.page}>
{/* main grid*/}
<div style={{ ...styles.container, ...styles.grid}}>
<Card
label="Total Events"
label="Total Activity"
value={summary?.total_events ?? "—"}
sublabel="Posts + comments"
style={{
@@ -74,15 +72,15 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
}}
/>
<Card
label="Unique Users"
label="Active People"
value={summary?.unique_users ?? "—"}
sublabel="Distinct authors"
sublabel="Distinct users"
style={{
gridColumn: "span 4"
}}
/>
<Card
label="Posts / Comments"
label="Posts vs Comments"
value={
summary
? `${summary.total_posts} / ${summary.total_comments}`
@@ -108,13 +106,13 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
/>
<Card
label="Lurker Ratio"
label="One-Time Users"
value={
typeof summary?.lurker_ratio === "number"
? `${Math.round(summary.lurker_ratio * 100)}%`
: "—"
}
sublabel="Users with only 1 event"
sublabel="Users with only one event"
style={{
gridColumn: "span 4"
}}
@@ -136,12 +134,12 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
{/* events per day */}
<div style={{ ...styles.card, gridColumn: "span 5" }}>
<h2 style={styles.sectionTitle}>Events per Day</h2>
<p style={styles.sectionSubtitle}>Trend of activity over time</p>
<h2 style={styles.sectionTitle}>Activity Over Time</h2>
<p style={styles.sectionSubtitle}>How much posting happened each day.</p>
<div style={styles.chartWrapper}>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={timeData?.events_per_day.filter((d) => new Date(d.date) >= new Date('2026-01-10'))}>
<LineChart data={timeData?.events_per_day ?? []}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
@@ -154,8 +152,8 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
{/* Word Cloud */}
<div style={{ ...styles.card, gridColumn: "span 4" }}>
<h2 style={styles.sectionTitle}>Word Cloud</h2>
<p style={styles.sectionSubtitle}>Most common terms across events</p>
<h2 style={styles.sectionTitle}>Common Words</h2>
<p style={styles.sectionSubtitle}>Frequently used words across the dataset.</p>
<div style={styles.chartWrapper}>
<ReactWordcloud
@@ -174,8 +172,8 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
<div style={{...styles.card, ...styles.scrollArea, gridColumn: "span 3",
}}
>
<h2 style={styles.sectionTitle}>Top Users</h2>
<p style={styles.sectionSubtitle}>Most active authors</p>
<h2 style={styles.sectionTitle}>Most Active Users</h2>
<p style={styles.sectionSubtitle}>Who posted the most events.</p>
<div style={styles.topUsersList}>
{userData?.top_users.slice(0, 100).map((item) => (
@@ -195,8 +193,8 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
{/* Heatmap */}
<div style={{ ...styles.card, gridColumn: "span 12" }}>
<h2 style={styles.sectionTitle}>Heatmap</h2>
<p style={styles.sectionSubtitle}>Activity density across time</p>
<h2 style={styles.sectionTitle}>Weekly Activity Pattern</h2>
<p style={styles.sectionSubtitle}>When activity tends to happen by weekday and hour.</p>
<div style={styles.heatmapWrapper}>
<ActivityHeatmap data={timeData?.weekday_hour_heatmap ?? []} />

View File

@@ -87,15 +87,15 @@ const UserStats = (props: { data: UserAnalysisResponse }) => {
style={{ gridColumn: "span 3" }}
/>
<Card
label="Interactions"
label="Replies"
value={totalInteractions.toLocaleString()}
sublabel="Filtered links (2+ interactions)"
sublabel="Links with at least 2 replies"
style={{ gridColumn: "span 3" }}
/>
<Card
label="Average Intensity"
label="Replies per Connected User"
value={avgInteractionsPerConnectedUser.toFixed(1)}
sublabel="Interactions per connected user"
sublabel="Average from visible graph links"
style={{ gridColumn: "span 3" }}
/>
<Card
@@ -106,13 +106,13 @@ const UserStats = (props: { data: UserAnalysisResponse }) => {
/>
<Card
label="Strongest Connection"
label="Strongest User Link"
value={strongestLink ? `${strongestLink.source} -> ${strongestLink.target}` : "—"}
sublabel={strongestLink ? `${strongestLink.value.toLocaleString()} interactions` : "No graph edges after filtering"}
sublabel={strongestLink ? `${strongestLink.value.toLocaleString()} replies` : "No graph links after filtering"}
style={{ gridColumn: "span 6" }}
/>
<Card
label="Most Reply-Driven User"
label="Most Comment-Heavy User"
value={highlyInteractiveUser?.author ?? "—"}
sublabel={
highlyInteractiveUser
@@ -125,7 +125,7 @@ const UserStats = (props: { data: UserAnalysisResponse }) => {
<div style={{ ...styles.card, gridColumn: "span 12" }}>
<h2 style={styles.sectionTitle}>User Interaction Graph</h2>
<p style={styles.sectionSubtitle}>
Nodes represent users and links represent conversation interactions.
Each node is a user, and each link shows replies between them.
</p>
<div ref={graphContainerRef} style={{ width: "100%", height: graphSize.height }}>
<ForceGraph3D