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 (
{visible.map((keyword) => (
-
{keyword.question}
{formatCount(keyword.aiSearchVolume)} vol.
))}
{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";
}