From c09bb30bc70a8d864c8e615ee835a0782e7a6447 Mon Sep 17 00:00:00 2001 From: Tom Smith <142233216+tomsmith8@users.noreply.github.com> Date: Mon, 18 May 2026 12:21:21 +0100 Subject: [PATCH] Skip duplicate description when schema title_key === description_key Section nodes (and any schema where title and description point to the same field) were rendering the same text twice. Add resolveNodeBody helper that returns undefined in that case, and an unescapeText helper for double-escaped \n / \uXXXX sequences coming from ingestion. --- src/components/feed/feed-card.tsx | 18 ++------ src/components/layout/node-preview-panel.tsx | 20 +++++---- src/lib/node-display.ts | 43 ++++++++++++++++++++ 3 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/components/feed/feed-card.tsx b/src/components/feed/feed-card.tsx index 65c20bb..b9d4452 100644 --- a/src/components/feed/feed-card.tsx +++ b/src/components/feed/feed-card.tsx @@ -2,7 +2,7 @@ import { Heart, MessageCircle, Repeat2, Eye, Play, BadgeCheck } from "lucide-react" import { parseTimestamp } from "@/lib/date-format" -import { pickString, resolveNodeTitle, resolveNodeThumbnail } from "@/lib/node-display" +import { pickString, resolveNodeBody, resolveNodeTitle, resolveNodeThumbnail, unescapeText } from "@/lib/node-display" import { getSchemaIconInfo } from "@/lib/schema-icons" import { cn, formatCompactNumber } from "@/lib/utils" import type { GraphNode } from "@/lib/graph-api" @@ -19,17 +19,6 @@ function timeAgo(value: unknown): string | null { return `${Math.floor(diff / (86400 * 30))}mo` } -function resolveBody(node: GraphNode): string | undefined { - const p = node.properties - return ( - pickString(p, "description") || - pickString(p, "summary") || - pickString(p, "bio") || - pickString(p, "claim_text") || - pickString(p, "text") - ) -} - interface FeedCardProps { node: GraphNode schemas: SchemaNode[] @@ -42,8 +31,9 @@ export function FeedCard({ node, schemas, selected, onSelect, onHover }: FeedCar const schema = schemas.find((s) => s.type === (node.node_type ?? "Unknown")) const { icon: Icon, accent } = getSchemaIconInfo(schema?.icon) const p = node.properties || {} - const title = resolveNodeTitle(node, schemas) - const body = resolveBody(node) + const title = unescapeText(resolveNodeTitle(node, schemas)) + const rawBody = resolveNodeBody(node, schemas) + const body = rawBody ? unescapeText(rawBody) : undefined const thumb = resolveNodeThumbnail(node) const avatar = pickString(p, "image_url") || thumb const handle = pickString(p, "twitter_handle") diff --git a/src/components/layout/node-preview-panel.tsx b/src/components/layout/node-preview-panel.tsx index 4ae122f..7273ed5 100644 --- a/src/components/layout/node-preview-panel.tsx +++ b/src/components/layout/node-preview-panel.tsx @@ -16,7 +16,7 @@ import { usePlayerStore } from "@/stores/player-store" import { useUserStore } from "@/stores/user-store" import { useModalStore } from "@/stores/modal-store" import { cn, displayNodeType, formatCompactNumber } from "@/lib/utils" -import { pickString, DISPLAY_KEY_FALLBACKS } from "@/lib/node-display" +import { pickString, unescapeText, DISPLAY_KEY_FALLBACKS } from "@/lib/node-display" import { getStatusBadge, isBlockedStatus } from "@/lib/node-status" import type { GraphNode, GraphData } from "@/lib/graph-api" import { getWatches, watchNode, unwatchNode } from "@/lib/watch-api" @@ -506,12 +506,18 @@ export function NodePreviewPanel({ node, onBack, schemas }: NodePreviewPanelProp } } if (!title) title = node.ref_id - - const rawDesc = pickString(props, schema?.description_key) - ?? pickString(props, "description") - const description = rawDesc && rawDesc.length > 500 - ? rawDesc.slice(0, 500) + "\u2026" - : rawDesc + title = unescapeText(title) + + // When the schema points title and description at the same field, the + // description is just a longer copy of the title \u2014 skip it. + const titleDescSame = + !!schema?.title_key && schema.title_key === schema.description_key + const rawDesc = titleDescSame + ? undefined + : pickString(props, schema?.description_key) ?? pickString(props, "description") + const description = rawDesc + ? unescapeText(rawDesc.length > 500 ? rawDesc.slice(0, 500) + "\u2026" : rawDesc) + : undefined const thumbnail = (props?.image_url ?? props?.thumbnail) as string | undefined // Hide the static thumbnail when this node is the one currently playing — diff --git a/src/lib/node-display.ts b/src/lib/node-display.ts index 9a1ba68..c631800 100644 --- a/src/lib/node-display.ts +++ b/src/lib/node-display.ts @@ -29,6 +29,49 @@ export function pickString( return typeof v === "string" && v.length > 0 ? v : undefined } +// Some ingested text arrives double-escaped (e.g. backend serializes a string +// that already contained literal `\n` / `\uXXXX` sequences, so the JSON value +// reaches us as `"\\n"` and renders the backslash). Decode the common forms. +export function unescapeText(s: string): string { + return s + .replace(/\\u([0-9a-fA-F]{4})/g, (_, code) => + String.fromCharCode(parseInt(code, 16)) + ) + .replace(/\\n/g, "\n") + .replace(/\\t/g, "\t") + .replace(/\\r/g, "") + .replace(/\\"/g, '"') + .replace(/\\\\/g, "\\") +} + +const BODY_KEY_FALLBACKS = [ + "description", + "summary", + "bio", + "claim_text", + "text", +] as const + +export function resolveNodeBody( + node: GraphNode, + schemas: SchemaNode[] +): string | undefined { + const schema = schemas.find((s) => s.type === (node.node_type ?? "Unknown")) + // When the schema reuses one field for both title and description, the body + // is just a longer copy of the title — skip it. + if (schema?.title_key && schema.title_key === schema.description_key) { + return undefined + } + const descKey = schema?.description_key + const fromSchema = pickString(node.properties, descKey) + if (fromSchema) return fromSchema + for (const key of BODY_KEY_FALLBACKS) { + const v = pickString(node.properties, key) + if (v) return v + } + return undefined +} + export function resolveNodeTitle(node: GraphNode, schemas: SchemaNode[]): string { const schema = schemas.find((s) => s.type === (node.node_type ?? "Unknown")) const titleKey = schema?.title_key ?? schema?.index