import { useState } from "react"; import { createColumnHelper, type Table } from "@tanstack/react-table"; import { Link } from "@tanstack/react-router"; import { ExternalLink, Sparkles } from "lucide-react"; import { AppDataTable } from "@/client/components/table/AppDataTable"; import { SortableHeader } from "@/client/components/table/SortableHeader"; import { HeaderHelpLabel } from "@/client/features/keywords/components"; import { numericNullsLast } from "@/client/components/table/nullSafeSort"; import { formatCount, PLATFORM_DOT_CLASS, PLATFORM_SHORT_LABEL, } from "@/client/features/ai-search/platformLabels"; import { formatUrlForDisplay } from "@/client/components/table/url"; import type { BrandLookupResult } from "@/types/schemas/ai-search"; type TopPageRow = BrandLookupResult["topPages"][number]; type TopQueryRow = BrandLookupResult["topQueries"][number]; type PlatformKey = TopPageRow["platform"]; /** Uppercase column header with a hover/focus popover explaining the column. */ function HeaderWithHelp({ label, helpText, }: { label: string; helpText: string; }) { return ( ); } const PLATFORM_HELP = "Which AI surface produced the answer — ChatGPT or Google AI Overview."; /** * Platform indicator used only when a table actually spans >1 platform. A dot + * short label replaces the old full-width pill that repeated identically on * every row. */ function PlatformCell({ platform }: { platform: PlatformKey }) { return ( {PLATFORM_SHORT_LABEL[platform]} ); } function urlPath(rawUrl: string): string { try { const url = new URL(rawUrl); const path = `${url.pathname}${url.search}`; return path === "/" ? "" : path; } catch { return ""; } } function normalizeDomain(value: string): string { return value.replace(/^www\./i, "").toLowerCase(); } /** * The lookup targets a domain with include_subdomains, so the target's own * pages can surface under any subdomain (docs.acme.com for acme.com) — those * must get the "You" badge too. */ function isTargetDomain(domain: string, targetDomain: string): boolean { const candidate = normalizeDomain(domain); const target = normalizeDomain(targetDomain); return candidate === target || candidate.endsWith(`.${target}`); } /** Domain-led cited page: bold domain + truncated path, links out. */ function PageUrlCell({ row, targetDomain, }: { row: TopPageRow; targetDomain: string | null; }) { const path = urlPath(row.url); const isOwn = targetDomain != null && row.domain != null && isTargetDomain(row.domain, targetDomain); return ( {row.domain ?? formatUrlForDisplay(row.url)} {isOwn ? ( You ) : null} {path ? ( {path} ) : null} ); } /** * The prompts (keywords) whose answers cited this page. Shows the top 3 inline; * if there are more, a "+N more" toggle reveals the rest. Each prompt links into * Prompt Explorer prefilled with it. */ function KeywordsCell({ keywords, projectId, brand, }: { keywords: TopPageRow["keywords"]; projectId: string; brand: string; }) { const [expanded, setExpanded] = useState(false); if (keywords.length === 0) { return ; } const visible = expanded ? keywords : keywords.slice(0, 3); const remaining = keywords.length - visible.length; return (
{keywords.length > 3 ? ( ) : null}
); } const pagesHelper = createColumnHelper(); const queriesHelper = createColumnHelper(); export function buildTopPagesColumns({ showPlatform, targetDomain, projectId, brand, }: { showPlatform: boolean; targetDomain: string | null; projectId: string; brand: string; }) { return [ pagesHelper.accessor("url", { id: "url", header: () => ( ), enableSorting: false, cell: ({ row }) => ( ), }), ...(showPlatform ? [ pagesHelper.accessor("platform", { id: "platform", header: () => ( ), enableSorting: false, cell: ({ getValue }) => , }), ] : []), pagesHelper.display({ id: "keywords", header: () => ( ), cell: ({ row }) => ( ), }), pagesHelper.accessor("capturedVolume", { id: "capturedVolume", header: ({ column }) => ( ), cell: ({ getValue }) => ( {formatCount(getValue())} ), sortingFn: numericNullsLast, sortDescFirst: true, }), ]; } export function buildTopQueriesColumns({ showPlatform, projectId, brand, }: { showPlatform: boolean; projectId: string; brand: string; }) { return [ queriesHelper.accessor("question", { id: "question", header: () => ( ), enableSorting: false, cell: ({ row }) => ( <>

{row.original.question}

{row.original.brandsMentioned.length > 0 ? (

Brands: {row.original.brandsMentioned.slice(0, 5).join(", ")}

) : null} ), }), ...(showPlatform ? [ queriesHelper.accessor("platform", { id: "platform", header: () => ( ), enableSorting: false, cell: ({ getValue }) => , }), ] : []), queriesHelper.accessor("aiSearchVolume", { id: "aiSearchVolume", header: ({ column }) => ( ), cell: ({ getValue }) => ( {formatCount(getValue())} ), sortingFn: numericNullsLast, sortDescFirst: true, }), queriesHelper.display({ id: "action", header: () => Actions, meta: { cellClassName: "w-px whitespace-nowrap text-right align-top" }, cell: ({ row }) => ( ), }), ]; } export function TopPagesTable({ table, emptyMessage = "No cited sources to show.", }: { table: Table; emptyMessage?: string; }) { if (table.getRowModel().rows.length === 0) { return (

{emptyMessage}

); } return ; } export function TopQueriesTable({ table, emptyMessage = "No matching queries found.", }: { table: Table; emptyMessage?: string; }) { if (table.getRowModel().rows.length === 0) { return (

{emptyMessage}

); } return ; } function BrandLookupTable({ table, urlLikeColumnId, }: { table: Table; urlLikeColumnId: string; }) { return ( "group transition-colors hover:bg-base-200/40"} getCellClassName={(_, columnId) => cellClassName( columnId, urlLikeColumnId, table.getColumn(columnId)?.getCanSort() ?? false, ) } /> ); } function cellClassName( columnId: string, urlLikeColumnId: string, isNumeric: boolean, ): string { if (columnId === urlLikeColumnId) { return "min-w-80 max-w-2xl align-top"; } if (columnId === "keywords") { return "max-w-lg align-top"; } if (isNumeric) { return "whitespace-nowrap text-right align-top"; } return "whitespace-nowrap align-top"; }