import { useMemo, useState } from "react"; import { evidenceUrl, type ResearchCandidateBatch, type ResearchSnapshot } from "@/lib/ai-research"; type CandidateStatus = ResearchCandidateBatch["records"][number]["status"]; const statusLabels: Record = { "pending-year": "成立年或主体待核", "pending-ai": "AI 主营待核", linked: "已关联公司", "existing-outside-cohort": "已关联范围外对照", "outside-cohort": "范围外候选", promotion: "推广活动", }; const pageSize = 20; const sourceNames: Record = { S01: "Y Combinator", S02: "TechCrunch AI", S04: "Product Hunt", S05: "Sacra", S10: "机器之心", S11: "36氪", S13: "TrustMRR", S14: "a16z Speedrun", }; const sourceLabel = (id: string) => `${id} · ${sourceNames[id] ?? "其他来源"}`; function SourceLink({ url, label }: { url: string | null; label: string }) { const safe = url && evidenceUrl(url); return safe ? {label} ↗ : {label}(链接未取得); } export default function ResearchCandidates({ data, onSelect, onChooseCategory }: { data: ResearchSnapshot; onSelect: (id: string) => void; onChooseCategory: (id: string) => void; }) { const [source, setSource] = useState(""); const [batchId, setBatchId] = useState(""); const [category, setCategory] = useState(""); const [status, setStatus] = useState("pending"); const [query, setQuery] = useState(""); const [page, setPage] = useState(0); const all = useMemo(() => (data.candidateBatches ?? []).flatMap((batch) => batch.records.map((record) => ({ batch, record }))), [data.candidateBatches]); const sources = [...new Set(all.map(({ batch }) => batch.sourceId))].sort(); const batches = (data.candidateBatches ?? []).filter((batch) => !source || batch.sourceId === source); const matched = all.filter(({ batch, record }) => (!source || batch.sourceId === source) && (!batchId || batch.id === batchId) && (!category || record.categoryId === category) && `${record.name} ${record.id} ${record.note}`.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase())); const pending = (value: CandidateStatus) => value === "pending-year" || value === "pending-ai"; const rows = matched.filter(({ record }) => !status || (status === "pending" ? pending(record.status) : record.status === status)); const companies = new Map(data.companies.map((company) => [company.id, company])); const evidence = new Map(data.evidence.map((item) => [item.id, item])); const categories = new Map(data.categories.map((item) => [item.id, item.name])); return

候选与缺项

展示已登记的有限来源批次及后续核查。默认显示成立年、主体或 AI 主营待核的条目;待核不是确认排除,也不计入公司表。每行是一次发现位置,同一公司或产品可以重复出现。

候选初分类与公司表数量分开,来源批次不等全年或全站名单。原始发现范围及后续结论见每行说明;暂无收入资料不作零,推广活动也不算公司。

{rows.length} 个发现位置匹配;不是独立公司数。

{rows.slice(page * pageSize, (page + 1) * pageSize).map(({ batch, record }) => { const company = record.companyId && companies.get(record.companyId); return ; })}
有限来源候选与处理记录;选择关联公司查看主数据,原始证据直接链接来源
候选初分类 / 状态来源及范围核查结论 / 缺项关联公司 / 证据
{record.id} {statusLabels[record.status]} {batch.id} · 读取 {batch.accessedAt}
发现批次范围

{batch.scope}

{batch.complete ? "该批次标记已完成;不代表来源全站覆盖。" : "该批次仍保留覆盖缺项。"}

{record.note} {company ? : 尚未关联公司主记录} {record.evidenceIds?.map((id) => { const item = evidence.get(id); return item ?

: null; })}
{!rows.length &&

当前筛选没有已登记的候选记录;不代表该来源或分类没有公司。

} {rows.length > pageSize && }
; }