diff --git a/frontend/src/components/CorpusExplorer.tsx b/frontend/src/components/CorpusExplorer.tsx
new file mode 100644
index 0000000..e382b51
--- /dev/null
+++ b/frontend/src/components/CorpusExplorer.tsx
@@ -0,0 +1,175 @@
+import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react";
+
+import StatsStyling from "../styles/stats_styling";
+import type { DatasetRecord } from "../utils/corpusExplorer";
+
+const styles = StatsStyling;
+
+const cleanText = (value: unknown) => {
+ if (typeof value !== "string") {
+ return "";
+ }
+
+ const trimmed = value.trim();
+ if (!trimmed) {
+ return "";
+ }
+
+ const lowered = trimmed.toLowerCase();
+ if (lowered === "nan" || lowered === "null" || lowered === "undefined") {
+ return "";
+ }
+
+ return trimmed;
+};
+
+const displayText = (value: unknown, fallback: string) => {
+ const cleaned = cleanText(value);
+ return cleaned || fallback;
+};
+
+type CorpusExplorerProps = {
+ open: boolean;
+ onClose: () => void;
+ title: string;
+ description: string;
+ records: DatasetRecord[];
+ loading: boolean;
+ error: string;
+ emptyMessage: string;
+};
+
+const formatRecordDate = (record: DatasetRecord) => {
+ if (typeof record.dt === "string" && record.dt) {
+ const date = new Date(record.dt);
+ if (!Number.isNaN(date.getTime())) {
+ return date.toLocaleString();
+ }
+ }
+
+ if (typeof record.date === "string" && record.date) {
+ return record.date;
+ }
+
+ if (typeof record.timestamp === "number") {
+ return new Date(record.timestamp * 1000).toLocaleString();
+ }
+
+ return "Unknown time";
+};
+
+const getRecordKey = (record: DatasetRecord, index: number) =>
+ String(record.id ?? record.post_id ?? `${record.author ?? "record"}-${index}`);
+
+const getRecordTitle = (record: DatasetRecord) => {
+ if (record.type === "comment") {
+ return "";
+ }
+
+ const title = cleanText(record.title);
+ if (title) {
+ return title;
+ }
+
+ const content = cleanText(record.content);
+ if (!content) {
+ return "Untitled record";
+ }
+
+ return content.length > 120 ? `${content.slice(0, 117)}...` : content;
+};
+
+const getRecordExcerpt = (record: DatasetRecord) => {
+ const content = cleanText(record.content);
+ if (!content) {
+ return "No content available.";
+ }
+
+ return content.length > 320 ? `${content.slice(0, 317)}...` : content;
+};
+
+const CorpusExplorer = ({
+ open,
+ onClose,
+ title,
+ description,
+ records,
+ loading,
+ error,
+ emptyMessage,
+}: CorpusExplorerProps) => (
+
+);
+
+export default CorpusExplorer;
diff --git a/frontend/src/components/CulturalStats.tsx b/frontend/src/components/CulturalStats.tsx
index e62b956..81e059d 100644
--- a/frontend/src/components/CulturalStats.tsx
+++ b/frontend/src/components/CulturalStats.tsx
@@ -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) => (
+
+);
+
+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
Most likely emotion when in-group wording is stronger.
Most likely emotion when out-group wording is stronger.
- How much posting happened each day. -
+How much posting happened each day.
@@ -213,7 +214,6 @@ const SummaryStats = ({
@@ -248,13 +247,6 @@ const SummaryStats = ({