From ac9ddd9c272cc28c76df8abe50b18cd2abd029f4 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 00:10:23 +0800 Subject: [PATCH] Expose source candidates and unresolved classification gaps --- .../docs/src/components/AICompanyResearch.tsx | 6 +- .../src/components/ResearchCandidates.tsx | 75 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 apps/docs/src/components/ResearchCandidates.tsx diff --git a/apps/docs/src/components/AICompanyResearch.tsx b/apps/docs/src/components/AICompanyResearch.tsx index 45fd7d3..434e558 100644 --- a/apps/docs/src/components/AICompanyResearch.tsx +++ b/apps/docs/src/components/AICompanyResearch.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import IncomeDistribution from "./IncomeDistribution"; import ArrDisclosures from "./ArrDisclosures"; import BusinessModelAssessment from "./BusinessModelAssessment"; +import ResearchCandidates from "./ResearchCandidates"; import { categorySummary, evidenceUrl, technicalReviewLabels, reviewCompanyIds, type ResearchEvidence, type ResearchReview, type ResearchSnapshot, } from "@/lib/ai-research"; @@ -57,7 +58,7 @@ export default function AICompanyResearch() { : router.isReady && typeof linkedCompany === "string" && data?.companies.some((c) => c.id === linkedCompany) ? linkedCompany : null; function setSelected(id: string | null) { setSelection({ route: router.asPath, id }); } - const [tab, setTab] = useState<"companies" | "revenue" | "models" | "sources" | "reviews">("companies"); + const [tab, setTab] = useState<"companies" | "candidates" | "revenue" | "models" | "sources" | "reviews">("companies"); const companySection = useRef(null); const detailSection = useRef(null); useEffect(() => { @@ -129,7 +130,7 @@ export default function AICompanyResearch() {

研究数据

- {([['companies','公司与产品'],['revenue','收入观测'],['models','商业模式初评'],['sources','原始证据'],['reviews','核查记录']] as const).map(([id,label]) => + {([['companies','公司与产品'],['candidates','候选与缺项'],['revenue','收入观测'],['models','商业模式初评'],['sources','原始证据'],['reviews','核查记录']] as const).map(([id,label]) => )}
{tab === "companies" && <> @@ -155,6 +156,7 @@ export default function AICompanyResearch() { {filtered.length > pageSize && } } {tab === "models" && } + {tab === "candidates" && } {tab === "revenue" && <> diff --git a/apps/docs/src/components/ResearchCandidates.tsx b/apps/docs/src/components/ResearchCandidates.tsx new file mode 100644 index 0000000..d9c3e1f --- /dev/null +++ b/apps/docs/src/components/ResearchCandidates.tsx @@ -0,0 +1,75 @@ +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 && } +
; +}