跳到主要内容

Algorithm Cards (算法卡片) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build a /algorithms page — a card-grid library of 12 bioinformatics algorithm explainers, visually consistent with the existing /skills page, with category filter, search, and a detail panel showing formula / flow / parameters / suitability.

Architecture: Static data file (_data.ts) + React page (index.tsx) + CSS Modules + detail panel component. No backend, no dynamic fetching — all 12 cards are bundled at build time for SSG (critical for GEO: AI crawlers see full content in static HTML). Mirrors the /skills page structure but with algorithm-specific data fields (formula, flow, key_params, suitable/not_suitable).

Tech Stack: React + TypeScript + Docusaurus 3 + CSS Modules. No new dependencies (KaTeX formula rendering uses inline Unicode + <code> blocks to avoid adding react-katex; revisit if richer math needed).

Global Constraints

  • Visual consistency: Reuse --biof3-* CSS variables from src/css/custom.css. Card grid / sidebar / search / detail panel layout mirrors src/pages/skills/.
  • SSG-first: All card content must be in the JS bundle (not fetched at runtime). AI crawlers (GPTBot / ClaudeBot) must see full content in static HTML.
  • GEO structure: Each card's one_liner, formula, suitable, not_suitable fields must render as plain text in HTML (not JS-only) so AI can extract them.
  • No new npm dependencies. Use existing react-markdown + remark-gfm (already in skills page) for detail panel rich text if needed.
  • Navbar entry: Add "算法卡片" to navbar config in docusaurus.config.ts.
  • Commit after each task. Conventional commits format: feat(algorithms): ...

File Structure

FileResponsibility
src/pages/algorithms/_data.tsTypeScript types + 12 algorithm card data objects. Single source of truth.
src/pages/algorithms/index.tsxMain page: header + stats + category sidebar + search + card grid + detail panel trigger.
src/pages/algorithms/algorithms.module.cssPage styles. Mirrors skills.module.css structure, reuses --biof3-* variables.
src/pages/algorithms/_AlgorithmDetailPanel.tsxRight-drawer detail panel: formula / flow diagram / key params / suitability / related links.
docusaurus.config.tsAdd navbar item for /algorithms.

Task 1: Data Layer — Types + 12 Algorithm Cards

Files:

  • Create: src/pages/algorithms/_data.ts

Interfaces:

  • Produces: AlgorithmCard interface, ALGORITHM_CATEGORIES array, ALGORITHMS array (12 items). Later tasks import from this file.

  • Step 1: Create the data file with types and all 12 cards

Create src/pages/algorithms/_data.ts:

// ============================================================
// /algorithms 页数据层 — 生信算法卡片
//
// 每张卡片解释一个工具背后的核心算法:公式、流程、关键参数、适合/不适合场景。
// 数据在 build 时打包进 JS bundle(SSG),AI 爬虫可直接从静态 HTML 抓取。
// ============================================================

export type AlgorithmCategory =
| 'differential-expression'
| 'enrichment'
| 'network'
| 'survival'
| 'single-cell'
| 'mutation';

export interface AlgorithmKeyParam {
name: string;
desc: string;
}

export interface AlgorithmCard {
id: string;
name: string; // 工具名,如 "DESeq2"
name_zh: string; // 算法中文名,如 "负二项模型差异分析"
category: AlgorithmCategory;
one_liner: string; // 一句话解释核心算法
formula: string; // 核心公式(Unicode + code block 渲染)
flow: string[]; // 输入 → 中间步骤 → 输出
key_params: AlgorithmKeyParam[];
suitable: string[]; // 适合场景
not_suitable: string[]; // 不适合场景
related_tool?: string; // /tools/#deseq2
related_tutorial?: string; // /docs/bulk-rnaseq/module02
related_code?: string; // static/scripts/... 路径
difficulty: 'beginner' | 'intermediate' | 'advanced';
}

export const ALGORITHM_CATEGORIES: {id: AlgorithmCategory; name_zh: string; name_en: string}[] = [
{id: 'differential-expression', name_zh: '差异分析', name_en: 'Differential Expression'},
{id: 'enrichment', name_zh: '富集分析', name_en: 'Enrichment'},
{id: 'network', name_zh: '网络分析', name_en: 'Network'},
{id: 'survival', name_zh: '生存分析', name_en: 'Survival'},
{id: 'single-cell', name_zh: '单细胞', name_en: 'Single-cell'},
{id: 'mutation', name_zh: '突变分析', name_en: 'Mutation'},
];

export const ALGORITHMS: AlgorithmCard[] = [
// 1. DESeq2
{
id: 'deseq2-nb',
name: 'DESeq2',
name_zh: '负二项模型差异分析',
category: 'differential-expression',
one_liner: '用负二项分布建模 count 数据的过度离散,估计每个基因的离散度 α,再用 Wald 检验算显著性,BH 校正得 padj。',
formula: 'K_ij ~ NB(μ_ij, α_i)\nμ_ij = s_j · q_ij\nα_i = f(μ̄_i) (mean-dispersion trend)',
flow: ['raw counts 矩阵', 'size factor 估计 (中位数比例)', '拟合 mean-dispersion 趋势', '每个基因离散度 α_i 收缩', 'Wald 检验: z = β / SE(β)', 'BH 校正 → padj'],
key_params: [
{name: 'α (dispersion)', desc: '基因特有离散度,无重复时无法估计'},
{name: 'shrinkage', desc: 'apeglm / ashr / normal,对 LFC 做收缩,低表达基因 LFC 往 0 拉'},
{name: 'padj 阈值', desc: '默认 0.05,BH 校正控制 FDR'},
],
suitable: ['bulk RNA-seq raw count', '有生物学重复 ≥3/组', '两组或多组比较'],
not_suitable: ['单细胞(太稀疏,用 MAST/Wilcoxon)', '已标准化数据(需原始 count)', '无生物学重复(离散度估不出)'],
related_tool: '/tools/#deseq2',
related_tutorial: '/docs/bulk-rnaseq/module02',
related_code: 'static/scripts/bulk/module02_complete_sci.R',
difficulty: 'intermediate',
},
// 2. edgeR
{
id: 'edger-nb',
name: 'edgeR',
name_zh: '负二项 + QL 检验',
category: 'differential-expression',
one_liner: '负二项分布建模,用 quasi-likelihood F 检验(glmQLFTest)比 Wald 更稳健,适合复杂设计和不等方差。',
formula: 'K_ij ~ NB(μ_ij, φ_i)\nφ_i = dispersion (gene-wise)\nQLF = (deviance reduction) / φ_i',
flow: ['raw counts', 'TMM 标准化', '估计 common / trended / tagwise dispersion', 'glmQLFit 拟合', 'glmQLFTest 检验', 'BH 校正 → FDR'],
key_params: [
{name: 'φ (dispersion)', desc: 'common / trended / tagwise 三档,tagwise 最精确'},
{name: 'robust', desc: 'robust=TRUE 对离群基因更稳'},
{name: 'TMM', desc: 'Trimmed Mean of M-values 标准化,默认方法'},
],
suitable: ['bulk RNA-seq raw count', '复杂设计矩阵(多因子 / 交互项)', '大样本(>20/组,比 DESeq2 快)'],
not_suitable: ['单细胞(太稀疏)', '已标准化数据', '小样本无重复'],
related_tutorial: '/docs/bulk-rnaseq/module02',
difficulty: 'intermediate',
},
// 3. limma-voom
{
id: 'limma-voom',
name: 'limma-voom',
name_zh: 'voom 加权线性模型',
category: 'differential-expression',
one_liner: '把 count 转成 log-CPM,用观测均值-方差关系给每个基因一个权重,再用 limma 的线性模型 + eBayes 检验。',
formula: 'logCPM_ij = log2(count_ij / library_size + 0.5)\nw_i = 1 / (a + b · μ̄_i)^0.5\nfit: lmFit → eBayes',
flow: ['raw counts', 'TMM 标准化', 'voom 计算 log-CPM + 权重', 'lmFit 拟合加权线性模型', 'eBayes 经验贝叶斯收缩', 'topTable → DE 基因'],
key_params: [
{name: 'voom 权重', desc: '低 count 基因降权,高 count 基因升权'},
{name: 'eBayes', desc: '借用所有基因信息收缩单个基因方差,小样本更稳'},
{name: 'design matrix', desc: '最灵活,支持任意设计矩阵'},
],
suitable: ['bulk RNA-seq', '多因子 / 交互项设计', '微阵列数据(不用 voom,直接 lmFit)'],
not_suitable: ['单细胞(太稀疏)', '无生物学重复'],
related_tutorial: '/docs/bulk-rnaseq/module02',
difficulty: 'intermediate',
},
// 4. GO/KEGG ORA
{
id: 'ora-hypergeometric',
name: 'GO/KEGG ORA',
name_zh: '超几何检验过表达分析',
category: 'enrichment',
one_liner: '给定差异基因列表和背景基因集,用超几何检验(Fisher 精确检验)判断每个基因集是否非随机富集。',
formula: 'P(X ≥ k) = Σ C(K,i)·C(N-K,n-i) / C(N,n)\nN=背景基因数, K=基因集大小, n=差异基因数, i=交集数',
flow: ['差异基因列表(有阈值)', '基因 ID 转换(SYMBOL → ENTREZID)', '对每个 GO/KEGG 术语做超几何检验', 'BH 校正 → padj', 'dotplot / barplot 可视化'],
key_params: [
{name: 'universe', desc: '背景基因集,默认所有表达基因,不是全基因组'},
{name: 'padj 阈值', desc: '通常 0.05,GO 术语多时放宽到 0.1'},
{name: 'gene ID 类型', desc: 'clusterProfiler 的 enrichGO/enrichKEGG 需要 ENTREZID'},
],
suitable: ['有明确差异基因列表', 'GO / KEGG / Reactome 通路库', '快速筛选显著通路'],
not_suitable: ['没有阈值的全基因排序(用 GSEA)', '连续型表型关联'],
related_tool: '/tools/#go-kegg',
related_tutorial: '/docs/bulk-rnaseq/module03',
difficulty: 'beginner',
},
// 5. GSEA
{
id: 'gsea-ks',
name: 'GSEA',
name_zh: '加权 Kolmogorov-Smirnov 富集',
category: 'enrichment',
one_liner: '不筛阈值,用全部基因按 log2FC 排序,通过加权 K-S 统计量看基因集在排序里的分布是否偏向两端。',
formula: 'ES(S) = Σ_{i∈S} |r_i|^p / N_R - Σ_{i∉S} 1/(N-N_H)\nNES = ES / mean(ES_permutations)\np = P(ES_perm ≥ ES_obs)',
flow: ['全部基因按 log2FC 排序', '遍历排序,基因在集合内 → ES 上升,不在 → ES 下降', '计算 ES (Enrichment Score)', '置换检验(gene-set 或 phenotype permutation)', '归一化 → NES', 'BH 校正 → padj'],
key_params: [
{name: 'p (权重指数)', desc: '默认 1,强调极端排名;=0 退化为标准 K-S'},
{name: 'minSize / maxSize', desc: '基因集大小过滤,默认 15-500'},
{name: 'permutation', desc: 'geneSet(小样本)或 phenotype(大样本)'},
],
suitable: ['没有明确阈值的全基因排序', '温和但协同的变化', '通路级分析'],
not_suitable: ['基因数很少(<1000)', '需要具体差异基因列表(用 ORA)'],
related_tool: '/tools/#gsea',
related_tutorial: '/docs/bulk-rnaseq/module03',
difficulty: 'intermediate',
},
// 6. WGCNA
{
id: 'wgcna',
name: 'WGCNA',
name_zh: '加权基因共表达网络',
category: 'network',
one_liner: '用 soft threshold 把相关矩阵变成加权邻接矩阵,算 TOM 距离后层次聚类切出模块,模块与性状关联。',
formula: 'a_ij = |cor(x_i, x_j)|^β (soft threshold)\nTOM_ij = (Σ_k a_ik·a_jk + a_ij) / (min(k_i,k_j) + 1 - a_ij)\n模块 = 层次聚类切 TOM → dynamicTreeCut',
flow: ['表达矩阵(方差过滤后)', '选 soft threshold β(scale-free 拟合)', '算邻接矩阵 → TOM', '层次聚类 → dynamicTreeCut 切模块', '模块 eigengene (ME) 与性状关联', 'hub 基因 = 模块内 kME 最高'],
key_params: [
{name: 'β (soft power)', desc: 'scale-free R² > 0.8 时的最小幂次,通常 6-12'},
{name: 'minModuleSize', desc: '默认 30,太小模块不稳定'},
{name: 'mergeCutHeight', desc: '模块合并阈值,默认 0.25'},
],
suitable: ['样本数 ≥15(越多越稳)', '探索性分析(无预设分组)', '找共表达模块和 hub 基因'],
not_suitable: ['样本太少(<15)', '需要严格统计推断(WGCNA 是探索性的)'],
related_tool: '/tools/#wgcna',
related_tutorial: '/docs/integration/module02',
difficulty: 'advanced',
},
// 7. LASSO-Cox
{
id: 'lasso-cox',
name: 'LASSO-Cox',
name_zh: 'L1 正则化 Cox 回归',
category: 'survival',
one_liner: '在 Cox 偏似然上加 L1 惩罚项,自动做特征选择,把不重要基因的系数压到 0,选出预后 signature。',
formula: 'min_β [ -Σ(δ_i·(x_i·β - log Σ_{j∈R(t_i)} exp(x_j·β))) + λ·Σ|β_k| ]',
flow: ['基因矩阵 + 生存数据 (OS, event)', '交叉验证选 λ(lambda.min / lambda.1se)', 'glmnet 拟合 LASSO-Cox', '非零系数基因 = signature', 'risk score = Σ β_k · x_k', 'KM 分组验证(中位数切高低风险)'],
key_params: [
{name: 'λ (penalty)', desc: 'lambda.min(最准)vs lambda.1se(最简),推荐 1se'},
{name: 'α=1', desc: 'α=1 是 LASSO(L1),α=0 是 ridge(L2),0<α<1 是 elastic net'},
{name: 'CV folds', desc: '默认 10 折交叉验证'},
],
suitable: ['高维基因 + 生存数据', '需要自动特征选择', '构建预后 risk score'],
not_suitable: ['样本数 < 事件数(会过拟合)', '无生存数据(用普通 LASSO)'],
related_tool: '/tools/#lasso-cox',
related_tutorial: '/docs/machine-learning/module04',
difficulty: 'advanced',
},
// 8. KM 生存
{
id: 'km-survival',
name: 'Kaplan-Meier',
name_zh: 'Kaplan-Meier 估计 + log-rank 检验',
category: 'survival',
one_liner: '非参数估计生存函数 S(t),用 log-rank 检验比较两组生存曲线是否有显著差异。',
formula: 'S(t) = Π_{t_i ≤ t} (1 - d_i/n_i)\nlog-rank: χ² = (O-E)² / E (每组分别算)',
flow: ['生存数据 (time, event) + 分组变量', '按时间排序事件', '每个事件时间点算 S(t) = Π(1 - d/n)', '画阶梯曲线', 'log-rank 检验比较组间差异', 'HR (hazard ratio) + 95% CI'],
key_params: [
{name: 'censoring', desc: '右删失最常见,删失样本在 S(t) 计算时留在 risk set 但不贡献事件'},
{name: 'log-rank', desc: '非参数检验,不假设分布,最常用'},
{name: '分组方式', desc: '中位数 / 最优截点(maxstat),中位数最常见但可能不是最优'},
],
suitable: ['两组生存比较', '时间到事件数据', '临床试验 / 预后分析'],
not_suitable: ['连续型预测变量(用 Cox 回归)', '竞争风险场景(用 Fine-Gray)'],
related_tool: '/tools/#km-survival',
related_tutorial: '/docs/machine-learning/module04',
difficulty: 'beginner',
},
// 9. ssGSEA 免疫浸润
{
id: 'ssgsea-immune',
name: 'ssGSEA',
name_zh: '单样本 GSEA 免疫浸润',
category: 'enrichment',
one_liner: '对每个样本独立做 GSEA,用免疫细胞 marker 基因集算富集分数,估计每个样本的免疫细胞比例。',
formula: 'ssGSEA(A, S) = Σ_{i∈S} (rank_i^w - |S|·(n+1)/2)\nrank_i = 基因 i 在样本 A 中的表达排名',
flow: ['表达矩阵(样本 × 基因)', '对每个样本独立排序基因', '用免疫 marker 基因集算 ssGSEA 分数', '分数 = 该样本中免疫细胞的相对丰度', 'boxplot / lollipop 可视化'],
key_params: [
{name: 'marker 基因集', desc: 'LM22 (22 免疫类型) / LM7 / 自定义'},
{name: 'w (权重)', desc: '默认 0.25,强调排名极端'},
{name: '标准化', desc: '分数跨样本比较前需 z-score 标准化'},
],
suitable: ['bulk RNA-seq 免疫浸润', '无参考数据时估计免疫比例', '肿瘤微环境分析'],
not_suitable: ['单细胞(用直接注释)', '需要绝对比例(用 CIBERSORT)'],
related_tool: '/tools/#immune',
related_tutorial: '/docs/bulk-rnaseq/module05',
difficulty: 'intermediate',
},
// 10. CellChat
{
id: 'cellchat',
name: 'CellChat',
name_zh: '配体受体细胞通讯',
category: 'single-cell',
one_liner: '用配体-受体对的共表达推断细胞间通讯,结合通路数据库算通讯概率和信号强度。',
formula: 'P(L-R) = (L_sender · R_receiver) · agonist · antagonist · cofactor\n通讯强度 = Σ P(L-R) across all L-R pairs in pathway',
flow: ['单细胞表达矩阵 + 细胞注释', '查 CellChatDB 配体受体对', '算每个 sender→receiver 的通讯概率', 'permutation 检验显著性', 'circle plot / chord plot 可视化', '通路富集分析'],
key_params: [
{name: '数据库', desc: 'human / mouse,Secreted Signaling / ECM-Receptor / Cell-Cell Contact'},
{name: 'population size', desc: '默认按细胞数加权,可设 population.size=FALSE 去除'},
{name: 'min cells', desc: '每组最少细胞数,默认 10'},
],
suitable: ['单细胞有细胞类型注释', '肿瘤微环境 / 发育 / 免疫通讯', '多样本比较'],
not_suitable: ['无注释数据', '空间转录组(用 spatial 方法)'],
related_tool: '/tools/#cellchat',
related_tutorial: '/docs/single-cell/module07',
difficulty: 'advanced',
},
// 11. Louvain 聚类
{
id: 'louvain-cluster',
name: 'Louvain 聚类',
name_zh: '模块度优化的社区发现',
category: 'single-cell',
one_liner: '在 KNN 图上用 Louvain 算法最大化模块度(modularity),把细胞分成社区 = 细胞类型。',
formula: 'Q = (1/2m) Σ [A_ij - k_i·k_j/2m] δ(c_i, c_j)\nLouvain: 迭代地将节点移到使 Q 增量最大的社区',
flow: ['PCA 降维后的表达矩阵', '建 KNN 图(k 邻居 + 距离权重)', 'Louvain 迭代:每个节点尝试移到邻居社区', '模块度 Q 最大化 → 收敛', '社区 = cluster = 细胞类型', 'resolution 控制粒度'],
key_params: [
{name: 'resolution', desc: '0.4-1.2,越高 cluster 越多越碎'},
{name: 'k (邻居数)', desc: '默认 20-50,影响图的连通性'},
{name: 'modularity', desc: 'Leiden 算法是 Louvain 改进版,更稳'},
],
suitable: ['单细胞聚类(Seurat 默认)', '大规模图(>10k 节点快)', '社区发现'],
not_suitable: ['连续型结构(用轨迹推断)', '小数据集(直接层次聚类更稳)'],
related_tool: '/tools/#seurat-standard',
related_tutorial: '/docs/single-cell/module03',
difficulty: 'intermediate',
},
// 12. 突变签名 NMF
{
id: 'mutation-nmf',
name: '突变签名 NMF',
name_zh: '非负矩阵分解突变签名',
category: 'mutation',
one_liner: '把突变频谱矩阵(96 trinotide × 样本)用 NMF 分解为签名矩阵和暴露矩阵,每个签名代表一种突变过程。',
formula: 'V ≈ W · H\nV: 96×samples (突变频谱)\nW: 96×r (签名)\nH: r×samples (暴露)\nmin ||V - W·H||² s.t. W,H ≥ 0',
flow: ['MAF 文件 → 提取 96 trinotide 突频谱', '选 rank r(COSMIC 签名数)', 'NMF 分解 → W (签名) + H (暴露)', '与 COSMIC v3 签名比对 (cosine similarity)', '每个样本的签名暴露量', 'stacked barplot 可视化'],
key_params: [
{name: 'rank r', desc: '用 NMF 评估指标选最优 r,或直接用 COSMIC 签名数'},
{name: 'COSMIC v3', desc: 'SBS / DBS / INDEL 三类签名,最常用 SBS'},
{name: 'cosine similarity', desc: '>0.85 视为匹配到已知签名'},
],
suitable: ['WES/WGS 突变数据 (MAF)', '肿瘤突变过程分析', '环境暴露推断'],
not_suitable: ['非突变数据', '样本突变数太少(<1000)'],
related_tool: '/tools/#maftools-mutation',
related_tutorial: '/docs/genomics/module05',
difficulty: 'advanced',
},
];
  • Step 2: Verify the file compiles

Run: npx tsc --noEmit src/pages/algorithms/_data.ts 2>&1 | grep "algorithms/_data" | head -5 Expected: No errors (or only unrelated node_modules errors).

  • Step 3: Commit
git add src/pages/algorithms/_data.ts
git commit -m "feat(algorithms): add data layer with 12 algorithm cards"

Task 2: CSS Module — Page Styles

Files:

  • Create: src/pages/algorithms/algorithms.module.css

Interfaces:

  • Produces: CSS classes consumed by index.tsx and _AlgorithmDetailPanel.tsx.

  • Step 1: Create the CSS file mirroring skills.module.css structure

Create src/pages/algorithms/algorithms.module.css:

/* ============================================================
/algorithms 页样式 — 仿 /skills 视觉,复用 --biof3-* CSS 变量
============================================================ */

.page {
background: #f8faf9;
min-height: calc(100vh - 60px);
padding: 0 0 48px;
}

/* ---- 页头 ---- */
.header {
max-width: 1280px;
margin: 0 auto;
padding: 24px 24px 16px;
}

.title {
font-size: 28px;
font-weight: 700;
margin: 0 0 6px;
color: #17201d;
}

.subtitle {
font-size: 14px;
color: #5c6b64;
margin: 0 0 14px;
line-height: 1.55;
max-width: 920px;
}

.statsRow {
display: flex;
gap: 24px;
flex-wrap: wrap;
font-size: 13px;
color: #5c6b64;
}

.statBadge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: #fff;
border: 1px solid #dfe8e3;
border-radius: 999px;
font-weight: 500;
}

.statBadge strong {
color: var(--biof3-teal);
font-weight: 600;
}

/* ---- 主内容区 ---- */
.main {
max-width: 1280px;
margin: 0 auto;
padding: 0 24px;
display: grid;
grid-template-columns: 240px 1fr;
gap: 24px;
}

/* ---- 左侧分类筛选 ---- */
.sidebar {
background: #fff;
border: 1px solid #dfe8e3;
border-radius: 8px;
padding: 16px;
height: fit-content;
position: sticky;
top: 80px;
}

.sidebarTitle {
font-size: 12px;
font-weight: 600;
color: #5c6b64;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0 0 12px;
}

.catList {
list-style: none;
margin: 0;
padding: 0;
}

.catItem {
padding: 6px 10px;
font-size: 12px;
color: #5c6b64;
border-radius: 4px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.1s;
margin-bottom: 2px;
}

.catItem:hover {
background: #f3f4f6;
color: #17201d;
}

.catItemActive {
background: #ecfdf5;
color: var(--biof3-teal);
font-weight: 600;
}

.catCount {
font-size: 10px;
background: #fff;
padding: 1px 6px;
border-radius: 999px;
border: 1px solid #dfe8e3;
color: #6b7280;
}

.catItemActive .catCount {
background: var(--biof3-teal);
color: #fff;
border-color: var(--biof3-teal);
}

.catReset {
display: block;
width: 100%;
margin-top: 8px;
padding: 6px 10px;
font-size: 11px;
background: #fff;
border: 1px solid #dfe8e3;
border-radius: 4px;
color: #6b7280;
cursor: pointer;
text-align: center;
}

.catReset:hover {
background: #f3f4f6;
color: #17201d;
}

/* ---- 搜索框 ---- */
.searchRow {
position: sticky;
top: 60px;
z-index: 5;
background: #f8faf9;
padding: 12px 0 8px;
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
}

.searchInput {
flex: 1;
min-width: 220px;
padding: 10px 14px;
border: 1px solid #dfe8e3;
border-radius: 6px;
font-size: 14px;
background: #fff;
transition: border-color 0.15s;
}

.searchInput:focus {
outline: none;
border-color: var(--biof3-teal);
box-shadow: 0 0 0 2px rgba(15, 118, 110, 0.1);
}

.searchStats {
font-size: 12px;
color: #5c6b64;
white-space: nowrap;
}

.searchStats strong {
color: var(--biof3-teal);
}

/* ---- 卡片网格 ---- */
.cardsGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
}

.card {
background: #fff;
border: 1px solid #dfe8e3;
border-radius: 8px;
padding: 16px;
transition: all 0.15s ease;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 8px;
}

.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-color: var(--biof3-teal);
}

.cardHeader {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
}

.categoryBadge {
display: inline-block;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
background: #ecfdf5;
color: var(--biof3-teal);
padding: 3px 8px;
border-radius: 4px;
}

.difficultyBadge {
font-size: 10px;
font-weight: 500;
padding: 2px 6px;
border-radius: 3px;
}

.difficultyBeginner { background: #d1fae5; color: #065f46; }
.difficultyIntermediate { background: #fef3c7; color: #92400e; }
.difficultyAdvanced { background: #fee2e2; color: #991b1b; }

.cardTitle {
font-size: 15px;
font-weight: 600;
color: #17201d;
margin: 0;
line-height: 1.4;
}

.cardNameZh {
font-size: 12px;
color: #6b7280;
font-weight: 400;
display: block;
margin-top: 2px;
}

.oneLiner {
font-size: 12px;
color: #5c6b64;
line-height: 1.55;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}

.cardFooter {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
padding-top: 8px;
border-top: 1px dashed #ecf0ee;
font-size: 11px;
color: #6b7280;
}

.flowPreview {
font-size: 11px;
color: #9ca3af;
font-family: monospace;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}

/* ---- 详情面板(右侧抽屉) ---- */
.detailOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 100;
display: flex;
justify-content: flex-end;
animation: fadeIn 0.15s;
}

@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

.detailPanel {
width: 56vw;
max-width: 880px;
min-width: 320px;
background: #fff;
height: 100vh;
overflow-y: auto;
padding: 0;
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
animation: slideIn 0.2s ease-out;
display: flex;
flex-direction: column;
}

@keyframes slideIn {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}

.detailHeader {
padding: 20px 24px 16px;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: flex-start;
justify-content: space-between;
}

.detailHeaderLeft h2 {
font-size: 20px;
font-weight: 700;
color: #17201d;
margin: 0;
}

.detailHeaderLeft .nameZh {
font-size: 13px;
color: #6b7280;
margin-top: 4px;
}

.detailClose {
background: none;
border: none;
font-size: 20px;
color: #9ca3af;
cursor: pointer;
padding: 4px;
border-radius: 4px;
}

.detailClose:hover {
background: #f3f4f6;
color: #374151;
}

.detailBody {
padding: 20px 24px;
flex: 1;
}

.detailSection {
margin-bottom: 24px;
}

.detailSectionTitle {
font-size: 11px;
font-weight: 600;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0 0 8px;
}

.oneLinerFull {
font-size: 14px;
color: #1f2937;
line-height: 1.7;
background: #f9fafb;
padding: 12px 14px;
border-left: 3px solid var(--biof3-teal);
border-radius: 0 6px 6px 0;
}

.formulaBlock {
background: #1e293b;
color: #e2e8f0;
padding: 16px;
border-radius: 6px;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 13px;
line-height: 1.8;
white-space: pre-wrap;
overflow-x: auto;
}

.flowDiagram {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}

.flowStep {
background: #ecfdf5;
color: var(--biof3-teal-dark);
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
}

.flowArrow {
color: #9ca3af;
font-size: 14px;
}

.paramTable {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}

.paramTable th {
text-align: left;
padding: 8px 10px;
background: #f9fafb;
border-bottom: 2px solid #e5e7eb;
font-weight: 600;
color: #374151;
}

.paramTable td {
padding: 8px 10px;
border-bottom: 1px solid #f3f4f6;
color: #5c6b64;
}

.paramTable td:first-child {
font-weight: 600;
color: #1f2937;
white-space: nowrap;
}

.suitabilityGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}

.suitabilityBox {
padding: 12px 14px;
border-radius: 6px;
font-size: 13px;
}

.suitabilityGood {
background: #ecfdf5;
border: 1px solid #a7f3d0;
}

.suitabilityBad {
background: #fef2f2;
border: 1px solid #fecaca;
}

.suitabilityBox ul {
margin: 8px 0 0;
padding-left: 18px;
}

.suitabilityBox li {
margin-bottom: 4px;
line-height: 1.5;
}

.suitabilityGood li { color: #065f46; }
.suitabilityBad li { color: #991b1b; }

.relatedLinks {
display: flex;
gap: 12px;
flex-wrap: wrap;
}

.relatedLink {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
text-decoration: none;
transition: all 0.15s;
}

.relatedLinkTool {
background: var(--biof3-teal);
color: #fff;
}

.relatedLinkTool:hover {
background: var(--biof3-teal-dark);
color: #fff;
text-decoration: none;
}

.relatedLinkTutorial {
background: #fff;
color: var(--biof3-teal);
border: 1px solid var(--biof3-teal);
}

.relatedLinkTutorial:hover {
background: #ecfdf5;
text-decoration: none;
}

.relatedLinkCode {
background: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
}

.relatedLinkCode:hover {
background: #e5e7eb;
text-decoration: none;
}

/* ---- 移动端 ---- */
@media (max-width: 768px) {
.main {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
}
.cardsGrid {
grid-template-columns: 1fr;
}
.title {
font-size: 24px;
}
.detailPanel {
width: 100vw;
max-width: 100vw;
}
.suitabilityGrid {
grid-template-columns: 1fr;
}
}
  • Step 2: Commit
git add src/pages/algorithms/algorithms.module.css
git commit -m "feat(algorithms): add page styles mirroring /skills layout"

Task 3: Detail Panel Component

Files:

  • Create: src/pages/algorithms/_AlgorithmDetailPanel.tsx

Interfaces:

  • Consumes: AlgorithmCard from _data.ts

  • Produces: AlgorithmDetailPanel component, used by index.tsx

  • Step 1: Create the detail panel component

Create src/pages/algorithms/_AlgorithmDetailPanel.tsx:

import React, {useEffect, useCallback} from 'react';
import {AlgorithmCard, ALGORITHM_CATEGORIES} from './_data';
import styles from './algorithms.module.css';

interface Props {
card: AlgorithmCard;
onClose: () => void;
}

export function AlgorithmDetailPanel({card, onClose}: Props) {
// ESC 关闭 + 锁滚动
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', onKey);
document.body.style.overflow = 'hidden';
return () => {
window.removeEventListener('keydown', onKey);
document.body.style.overflow = '';
};
}, [onClose]);

const handleOverlayClick = useCallback((e: React.MouseEvent) => {
if (e.target === e.currentTarget) onClose();
}, [onClose]);

const cat = ALGORITHM_CATEGORIES.find(c => c.id === card.category);

return (
<div className={styles.detailOverlay} onClick={handleOverlayClick}>
<div className={styles.detailPanel}>
{/* Header */}
<div className={styles.detailHeader}>
<div className={styles.detailHeaderLeft}>
<h2>{card.name}</h2>
<div className={styles.nameZh}>{card.name_zh}</div>
</div>
<button className={styles.detailClose} onClick={onClose} aria-label="关闭"></button>
</div>

{/* Body */}
<div className={styles.detailBody}>
{/* 一句话 */}
<div className={styles.detailSection}>
<h3 className={styles.detailSectionTitle}>核心算法</h3>
<div className={styles.oneLinerFull}>{card.one_liner}</div>
</div>

{/* 公式 */}
<div className={styles.detailSection}>
<h3 className={styles.detailSectionTitle}>核心公式</h3>
<pre className={styles.formulaBlock}>{card.formula}</pre>
</div>

{/* 流程 */}
<div className={styles.detailSection}>
<h3 className={styles.detailSectionTitle}>计算流程</h3>
<div className={styles.flowDiagram}>
{card.flow.map((step, i) => (
<React.Fragment key={i}>
<span className={styles.flowStep}>{step}</span>
{i < card.flow.length - 1 && <span className={styles.flowArrow}></span>}
</React.Fragment>
))}
</div>
</div>

{/* 关键参数 */}
<div className={styles.detailSection}>
<h3 className={styles.detailSectionTitle}>关键参数</h3>
<table className={styles.paramTable}>
<thead>
<tr><th>参数</th><th>说明</th></tr>
</thead>
<tbody>
{card.key_params.map((p, i) => (
<tr key={i}>
<td>{p.name}</td>
<td>{p.desc}</td>
</tr>
))}
</tbody>
</table>
</div>

{/* 适合 / 不适合 */}
<div className={styles.detailSection}>
<h3 className={styles.detailSectionTitle}>适用场景</h3>
<div className={styles.suitabilityGrid}>
<div className={`${styles.suitabilityBox} ${styles.suitabilityGood}`}>
<strong>✅ 适合</strong>
<ul>{card.suitable.map((s, i) => <li key={i}>{s}</li>)}</ul>
</div>
<div className={`${styles.suitabilityBox} ${styles.suitabilityBad}`}>
<strong>❌ 不适合</strong>
<ul>{card.not_suitable.map((s, i) => <li key={i}>{s}</li>)}</ul>
</div>
</div>
</div>

{/* 相关链接 */}
<div className={styles.detailSection}>
<h3 className={styles.detailSectionTitle}>相关资源</h3>
<div className={styles.relatedLinks}>
{card.related_tool && (
<a href={card.related_tool} className={`${styles.relatedLink} ${styles.relatedLinkTool}`}>
🧪 在线体验
</a>
)}
{card.related_tutorial && (
<a href={card.related_tutorial} className={`${styles.relatedLink} ${styles.relatedLinkTutorial}`}>
📖 看教程
</a>
)}
{card.related_code && (
<a href={`/${card.related_code}`} className={`${styles.relatedLink} ${styles.relatedLinkCode}`}>
💻 看代码
</a>
)}
</div>
</div>
</div>
</div>
</div>
);
}
  • Step 2: Commit
git add src/pages/algorithms/_AlgorithmDetailPanel.tsx
git commit -m "feat(algorithms): add detail panel with formula/flow/params/suitability"

Task 4: Main Page — Card Grid + Search + Filter

Files:

  • Create: src/pages/algorithms/index.tsx

Interfaces:

  • Consumes: ALGORITHMS, ALGORITHM_CATEGORIES, AlgorithmCard from _data.ts; AlgorithmDetailPanel from _AlgorithmDetailPanel.tsx; styles from algorithms.module.css.

  • Produces: default-exported React page component rendered at /algorithms.

  • Step 1: Create the main page

Create src/pages/algorithms/index.tsx:

import React, {useMemo, useState, useDeferredValue} from 'react';
import Layout from '@theme/Layout';
import BrowserOnly from '@docusaurus/BrowserOnly';
import {ALGORITHMS, ALGORITHM_CATEGORIES, type AlgorithmCard, type AlgorithmCategory} from './_data';
import {AlgorithmDetailPanel} from './_AlgorithmDetailPanel';
import styles from './algorithms.module.css';

function AlgorithmCardView({card, onClick}: {card: AlgorithmCard; onClick: () => void}) {
const cat = ALGORITHM_CATEGORIES.find(c => c.id === card.category);
const diffClass =
card.difficulty === 'beginner' ? styles.difficultyBeginner
: card.difficulty === 'intermediate' ? styles.difficultyIntermediate
: styles.difficultyAdvanced;

return (
<div
className={styles.card}
onClick={onClick}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onClick(); }}
>
<div className={styles.cardHeader}>
<span className={styles.categoryBadge}>{cat?.name_zh || card.category}</span>
<span className={`${styles.difficultyBadge} ${diffClass}`}>
{card.difficulty === 'beginner' ? '入门' : card.difficulty === 'intermediate' ? '进阶' : '高级'}
</span>
</div>
<h3 className={styles.cardTitle}>
{card.name}
<span className={styles.cardNameZh}>{card.name_zh}</span>
</h3>
<p className={styles.oneLiner}>{card.one_liner}</p>
<div className={styles.cardFooter}>
<span className={styles.flowPreview}>{card.flow.join(' → ')}</span>
</div>
</div>
);
}

function AlgorithmsPageInner() {
const [selectedCat, setSelectedCat] = useState<AlgorithmCategory | null>(null);
const [search, setSearch] = useState('');
const [activeCard, setActiveCard] = useState<AlgorithmCard | null>(null);
const deferredSearch = useDeferredValue(search);

const filtered = useMemo(() => {
let list = ALGORITHMS;
if (selectedCat) list = list.filter(a => a.category === selectedCat);
const q = deferredSearch.trim().toLowerCase();
if (q) {
list = list.filter(a =>
`${a.name} ${a.name_zh} ${a.one_liner} ${a.flow.join(' ')} ${a.suitable.join(' ')} ${a.not_suitable.join(' ')}`.toLowerCase().includes(q)
);
}
return list;
}, [selectedCat, deferredSearch]);

const catCounts = useMemo(() => {
const counts: Record<string, number> = {};
for (const a of ALGORITHMS) counts[a.category] = (counts[a.category] || 0) + 1;
return counts;
}, []);

return (
<Layout title="算法卡片 — 生信工具核心算法" description="12 个生信工具的核心算法解释:公式、流程、参数、适用场景。DESeq2 负二项、GSEA 加权 K-S、WGCNA 加权网络等。">
<div className={styles.page}>
<header className={styles.header}>
<h1 className={styles.title}>算法卡片</h1>
<p className={styles.subtitle}>
每张卡片解释一个生信工具背后的核心算法——公式、计算流程、关键参数、适合和不适合的场景。
不只是"怎么用",更是"为什么这样算"
</p>
<div className={styles.statsRow}>
<span className={styles.statBadge}><strong>{ALGORITHMS.length}</strong> 算法卡片</span>
<span className={styles.statBadge}><strong>{ALGORITHM_CATEGORIES.length}</strong> 分类</span>
</div>
</header>

<main className={styles.main}>
{/* 分类侧栏 */}
<aside className={styles.sidebar}>
<h2 className={styles.sidebarTitle}>分类</h2>
<ul className={styles.catList}>
<li
className={`${styles.catItem} ${selectedCat === null ? styles.catItemActive : ''}`}
onClick={() => setSelectedCat(null)}
>
<span>全部</span>
<span className={styles.catCount}>{ALGORITHMS.length}</span>
</li>
{ALGORITHM_CATEGORIES.map(cat => (
<li
key={cat.id}
className={`${styles.catItem} ${selectedCat === cat.id ? styles.catItemActive : ''}`}
onClick={() => setSelectedCat(cat.id)}
>
<span>{cat.name_zh}</span>
<span className={styles.catCount}>{catCounts[cat.id] || 0}</span>
</li>
))}
</ul>
{selectedCat && (
<button className={styles.catReset} onClick={() => setSelectedCat(null)}>清空筛选</button>
)}
</aside>

<div>
{/* 搜索 */}
<div className={styles.searchRow}>
<input
className={styles.searchInput}
type="search"
placeholder="搜索算法(名称 / 描述 / 流程 / 适用场景)..."
value={search}
onChange={e => setSearch(e.target.value)}
/>
<span className={styles.searchStats}>
<strong>{filtered.length}</strong> / {ALGORITHMS.length}
</span>
</div>

{/* 卡片网格 */}
<div className={styles.cardsGrid}>
{filtered.map(card => (
<AlgorithmCardView key={card.id} card={card} onClick={() => setActiveCard(card)} />
))}
</div>

{filtered.length === 0 && (
<p style={{textAlign: 'center', color: '#9ca3af', padding: '40px 0'}}>
没有匹配的算法卡片
</p>
)}
</div>
</main>

{activeCard && <AlgorithmDetailPanel card={activeCard} onClose={() => setActiveCard(null)} />}
</div>
</Layout>
);
}

export default function AlgorithmsPage() {
return (
<BrowserOnly>
{() => <AlgorithmsPageInner />}
</BrowserOnly>
);
}
  • Step 2: Verify build compiles

Run: npm run build 2>&1 | grep -E "SUCCESS|ERROR|algorithms" | head -10 Expected: [SUCCESS] Generated static files in "build".

  • Step 3: Commit
git add src/pages/algorithms/index.tsx
git commit -m "feat(algorithms): add main page with card grid, search, and category filter"

Task 5: Navbar Entry + Build Verification + Deploy

Files:

  • Modify: docusaurus.config.ts (navbar items array)

  • Step 1: Add navbar item

In docusaurus.config.ts, find the navbar.items array. Add a new item for algorithms. Place it after the existing skills item (or after figcode if no skills item exists):

{
label: '算法卡片',
to: '/algorithms',
},

Look for the existing navbar items structure (items with label and to or href) and insert this entry in a logical position (e.g., after the tools/skills entries, before the "更多" dropdown).

  • Step 2: Build and verify

Run: npm run build 2>&1 | tail -5 Expected: [SUCCESS] Generated static files in "build".

Verify the page was generated: Run: ls build/algorithms/index.html Expected: file exists.

  • Step 3: Commit
git add docusaurus.config.ts
git commit -m "feat(algorithms): add navbar entry for /algorithms page"
  • Step 4: Deploy to production

Run: ./deploy.sh --site Expected: ✓ https://biof3.com/algorithms/ → HTTP 200

  • Step 5: Verify production

Run: curl -sI https://biof3.com/algorithms/ | head -3 Expected: HTTP/2 200


Self-Review

1. Spec coverage:

  • ✅ Card grid with 12 algorithm cards — Task 1 (data) + Task 4 (page)
  • ✅ Category filter sidebar — Task 4
  • ✅ Full-text search — Task 4
  • ✅ Detail panel with formula/flow/params/suitability — Task 3
  • ✅ Related links (tool/tutorial/code) — Task 1 (data) + Task 3 (panel)
  • ✅ Visual consistency with /skills — Task 2 (CSS reuses --biof3-*)
  • ✅ Navbar entry — Task 5
  • ✅ SSG for GEO (all data in bundle, not runtime fetch) — Task 1 (static data) + Task 4 (no fetch)
  • ✅ Deploy — Task 5

2. Placeholder scan: No TBD/TODO/"implement later" found. All 12 cards have complete data. All code blocks are complete.

3. Type consistency: AlgorithmCard interface defined in Task 1, used in Task 3 (_AlgorithmDetailPanel.tsx) and Task 4 (index.tsx) with matching field names. ALGORITHM_CATEGORIES used in Task 3 and Task 4 with matching id/name_zh fields. AlgorithmCategory type used consistently.

AI 组学实践

让 AI 带我实战这一篇

AI 会读这篇文章后给你 3-5 步学习计划, 逐步带你学完,最后出 1-3 道题验证你掌握得怎么样。 登录后 AI 才能记住你的进度。

静态文件

离线资料下载

手册 HTML / PDF 已在后台预生成,点击后直接下载网站静态资源。