feat(frontend): implement corpus explorer

This allows you to view the posts & comments associated with a specific aggregate.
This commit is contained in:
2026-04-01 00:04:25 +01:00
parent 1dde5f7b08
commit b270ed03ae
11 changed files with 1064 additions and 179 deletions

View File

@@ -1,14 +1,34 @@
import Card from "./Card";
import StatsStyling from "../styles/stats_styling";
import type { CulturalAnalysisResponse } from "../types/ApiTypes";
import {
buildCertaintySpec,
buildDeonticSpec,
buildEntitySpec,
buildHedgeSpec,
buildIdentityBucketSpec,
buildPermissionSpec,
getExplorerButtonStyle,
type CorpusExplorerSpec,
} from "../utils/corpusExplorer";
const styles = StatsStyling;
type CulturalStatsProps = {
data: CulturalAnalysisResponse;
onExplore: (spec: CorpusExplorerSpec) => void;
};
const CulturalStats = ({ data }: CulturalStatsProps) => {
const renderExploreButton = (onClick: () => void) => (
<button
onClick={onClick}
style={{ ...styles.buttonSecondary, ...getExplorerButtonStyle() }}
>
Explore
</button>
);
const CulturalStats = ({ data, onExplore }: CulturalStatsProps) => {
const identity = data.identity_markers;
const stance = data.stance_markers;
const inGroupWords = identity?.in_group_usage ?? 0;
@@ -30,7 +50,7 @@ const CulturalStats = ({ data }: CulturalStatsProps) => {
const topEmotion = (emotionAvg: Record<string, number> | undefined) => {
const entries = Object.entries(emotionAvg ?? {});
if (!entries.length) {
return "";
return "-";
}
entries.sort((a, b) => b[1] - a[1]);
@@ -64,21 +84,30 @@ const CulturalStats = ({ data }: CulturalStatsProps) => {
/>
<Card
label="In-Group Posts"
value={identity?.in_group_posts?.toLocaleString() ?? ""}
value={identity?.in_group_posts?.toLocaleString() ?? "-"}
sublabel='Posts leaning toward "us" language'
rightSlot={renderExploreButton(() =>
onExplore(buildIdentityBucketSpec("in")),
)}
style={{ gridColumn: "span 3" }}
/>
<Card
label="Out-Group Posts"
value={identity?.out_group_posts?.toLocaleString() ?? ""}
value={identity?.out_group_posts?.toLocaleString() ?? "-"}
sublabel='Posts leaning toward "them" language'
rightSlot={renderExploreButton(() =>
onExplore(buildIdentityBucketSpec("out")),
)}
style={{ gridColumn: "span 3" }}
/>
<Card
label="Balanced Posts"
value={identity?.tie_posts?.toLocaleString() ?? ""}
value={identity?.tie_posts?.toLocaleString() ?? "-"}
sublabel="Posts with equal us/them signals"
rightSlot={renderExploreButton(() =>
onExplore(buildIdentityBucketSpec("tie")),
)}
style={{ gridColumn: "span 3" }}
/>
<Card
@@ -90,7 +119,7 @@ const CulturalStats = ({ data }: CulturalStatsProps) => {
<Card
label="In-Group Share"
value={
inGroupWordRate === null ? "" : `${inGroupWordRate.toFixed(2)}%`
inGroupWordRate === null ? "-" : `${inGroupWordRate.toFixed(2)}%`
}
sublabel="Share of all words"
style={{ gridColumn: "span 3" }}
@@ -98,7 +127,7 @@ const CulturalStats = ({ data }: CulturalStatsProps) => {
<Card
label="Out-Group Share"
value={
outGroupWordRate === null ? "" : `${outGroupWordRate.toFixed(2)}%`
outGroupWordRate === null ? "-" : `${outGroupWordRate.toFixed(2)}%`
}
sublabel="Share of all words"
style={{ gridColumn: "span 3" }}
@@ -106,42 +135,46 @@ const CulturalStats = ({ data }: CulturalStatsProps) => {
<Card
label="Hedging Words"
value={stance?.hedge_total?.toLocaleString() ?? ""}
value={stance?.hedge_total?.toLocaleString() ?? "-"}
sublabel={
typeof stance?.hedge_per_1k_tokens === "number"
? `${stance.hedge_per_1k_tokens.toFixed(1)} per 1k words`
: "Word frequency"
}
rightSlot={renderExploreButton(() => onExplore(buildHedgeSpec()))}
style={{ gridColumn: "span 3" }}
/>
<Card
label="Certainty Words"
value={stance?.certainty_total?.toLocaleString() ?? ""}
value={stance?.certainty_total?.toLocaleString() ?? "-"}
sublabel={
typeof stance?.certainty_per_1k_tokens === "number"
? `${stance.certainty_per_1k_tokens.toFixed(1)} per 1k words`
: "Word frequency"
}
rightSlot={renderExploreButton(() => onExplore(buildCertaintySpec()))}
style={{ gridColumn: "span 3" }}
/>
<Card
label="Need/Should Words"
value={stance?.deontic_total?.toLocaleString() ?? ""}
value={stance?.deontic_total?.toLocaleString() ?? "-"}
sublabel={
typeof stance?.deontic_per_1k_tokens === "number"
? `${stance.deontic_per_1k_tokens.toFixed(1)} per 1k words`
: "Word frequency"
}
rightSlot={renderExploreButton(() => onExplore(buildDeonticSpec()))}
style={{ gridColumn: "span 3" }}
/>
<Card
label="Permission Words"
value={stance?.permission_total?.toLocaleString() ?? ""}
value={stance?.permission_total?.toLocaleString() ?? "-"}
sublabel={
typeof stance?.permission_per_1k_tokens === "number"
? `${stance.permission_per_1k_tokens.toFixed(1)} per 1k words`
: "Word frequency"
}
rightSlot={renderExploreButton(() => onExplore(buildPermissionSpec()))}
style={{ gridColumn: "span 3" }}
/>
@@ -150,8 +183,14 @@ const CulturalStats = ({ data }: CulturalStatsProps) => {
<p style={styles.sectionSubtitle}>
Most likely emotion when in-group wording is stronger.
</p>
<div style={styles.topUserName}>
{topEmotion(identity?.in_group_emotion_avg)}
<div style={styles.topUserName}>{topEmotion(identity?.in_group_emotion_avg)}</div>
<div style={{ marginTop: 12 }}>
<button
onClick={() => onExplore(buildIdentityBucketSpec("in"))}
style={styles.buttonSecondary}
>
Explore records
</button>
</div>
</div>
@@ -160,8 +199,14 @@ const CulturalStats = ({ data }: CulturalStatsProps) => {
<p style={styles.sectionSubtitle}>
Most likely emotion when out-group wording is stronger.
</p>
<div style={styles.topUserName}>
{topEmotion(identity?.out_group_emotion_avg)}
<div style={styles.topUserName}>{topEmotion(identity?.out_group_emotion_avg)}</div>
<div style={{ marginTop: 12 }}>
<button
onClick={() => onExplore(buildIdentityBucketSpec("out"))}
style={styles.buttonSecondary}
>
Explore records
</button>
</div>
</div>
@@ -171,9 +216,7 @@ const CulturalStats = ({ data }: CulturalStatsProps) => {
Most mentioned entities and the mood that appears most with each.
</p>
{!entities.length ? (
<div style={styles.topUserMeta}>
No entity-level cultural data available.
</div>
<div style={styles.topUserMeta}>No entity-level cultural data available.</div>
) : (
<div
style={{
@@ -183,7 +226,11 @@ const CulturalStats = ({ data }: CulturalStatsProps) => {
}}
>
{entities.map(([entity, aggregate]) => (
<div key={entity} style={styles.topUserItem}>
<div
key={entity}
style={{ ...styles.topUserItem, cursor: "pointer" }}
onClick={() => onExplore(buildEntitySpec(entity))}
>
<div style={styles.topUserName}>{entity}</div>
<div style={styles.topUserMeta}>
{aggregate.post_count.toLocaleString()} posts Likely mood:{" "}