import React, { useEffect, useMemo, useState } from 'react';
import { THEME, tempColor, pHColor, saltColor } from '../theme.js';
import { MediaConfBar, OxygenConfArc, IntervalBar, MonoTag, SourceBadge } from './Primitives.jsx';
function ModeStrip({ mode, setMode }) {
const focused = mode === 'focused';
return (
setMode('focused')} style={{
flex: 1, padding: '14px 28px', textAlign: 'left', border: 'none', cursor: 'pointer',
background: focused ? '#ede4cd' : 'transparent',
borderBottom: focused ? `2px solid ${THEME.accent}` : '2px solid transparent',
font: `400 13px ${THEME.font}`, color: focused ? THEME.ink : THEME.inkFaint,
}}>
1,294
truly never-cultured
{focused && NCBI name starts with "uncultured" }
setMode('broad')} style={{
flex: 1, padding: '14px 28px', textAlign: 'left', border: 'none', cursor: 'pointer',
background: !focused ? '#e8e0c8' : 'transparent',
borderBottom: !focused ? `2px solid ${THEME.accent}` : '2px solid transparent',
borderLeft: `1px solid ${THEME.rule}`,
font: `400 13px ${THEME.font}`, color: !focused ? THEME.ink : THEME.inkFaint,
}}>
5,000
all candidates
{!focused && includes 3,706 named-but-absent-from-BacDive }
);
}
const FILTERS = [
{ id: 'all', label: 'All', test: () => true },
{ id: 'thermo', label: 'Thermophiles', sub: '>55°C', test: (m) => m.T_opt > 55 },
{ id: 'psychro', label: 'Psychrophiles', sub: '<15°C', test: (m) => m.T_opt < 15 },
{ id: 'anaerobe', label: 'Anaerobes', test: (m) => (m.O2 || '').toLowerCase().includes('anaerobe') },
{ id: 'halo', label: 'Halotolerant', sub: '>3% NaCl', test: (m) => m.salt > 3 },
];
function QuickFilters({ active, setActive, count }) {
return (
{FILTERS.map((f) => (
setActive(f.id)} style={{
padding: '6px 12px', border: `1px solid ${active === f.id ? THEME.ink : THEME.rule}`,
background: active === f.id ? THEME.ink : 'transparent',
color: active === f.id ? THEME.paper : THEME.ink,
font: `400 12px ${THEME.font}`, cursor: 'pointer', borderRadius: 2,
display: 'inline-flex', alignItems: 'baseline', gap: 6,
}}>
{f.label}
{f.sub && {f.sub} }
))}
showing {count} candidates · sorted by top-medium confidence
);
}
function PhenoMicro({ label, value, unit = '', color }) {
// Synthetic interval: ±5% of scale to show the primitive without per-row data
// (Catalog has point estimates only; real intervals show in Predict + Detail)
return (
);
}
function FeaturedCard({ m, onSelect }) {
return (
onSelect(m)} style={{
background: THEME.paper, border: `1px solid ${THEME.rule}`, padding: '16px 18px',
cursor: 'pointer', display: 'flex', flexDirection: 'column', gap: 12, transition: 'border-color 120ms',
}} onMouseEnter={(e) => (e.currentTarget.style.borderColor = THEME.ink)}
onMouseLeave={(e) => (e.currentTarget.style.borderColor = THEME.rule)}>
{m.accession} · {m.phylum}
{m.name}
Try this medium
{m.top_medium_id}
{m.top_medium_name}
);
}
function TableRow({ m, onSelect, isLast }) {
return (
onSelect(m)} style={{
cursor: 'pointer', borderBottom: isLast ? 'none' : `1px solid ${THEME.ruleSoft}`,
}} onMouseEnter={(e) => (e.currentTarget.style.background = '#ede5cd')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}>
{m.accession}
{m.name}
{m.phylum}
{m.top_medium_id}
{m.top_medium_name}
{m.T_opt.toFixed(0)}°C
{m.pH.toFixed(1)}
{m.O2}
{m.salt.toFixed(1)}%
{m.completeness.toFixed(0)}
);
}
export default function Catalog({ data, onSelect }) {
const [mode, setMode] = useState('focused');
const [filter, setFilter] = useState('all');
const [search, setSearch] = useState('');
const filtered = useMemo(() => {
let list = data || [];
if (mode === 'focused') list = list.filter((m) => m.truly_uncultured);
if (search) {
const q = search.toLowerCase();
list = list.filter((m) => m.name.toLowerCase().includes(q) || m.accession.toLowerCase().includes(q));
}
const f = FILTERS.find((x) => x.id === filter)?.test || (() => true);
list = list.filter(f);
return [...list].sort((a, b) => b.top_confidence - a.top_confidence);
}, [data, mode, filter, search]);
const featured = filtered.slice(0, 6);
const rest = filtered.slice(6, 86);
return (
{featured.length > 0 && (
Top {featured.length} picks
by media confidence
{featured.map((m) => )}
)}
{rest.length > 0 && (
Remaining {filtered.length - 6}
{['Accession', 'Organism', 'Phylum', 'Try this medium', 'Conf.', 'T', 'pH', 'O₂', 'Salt', 'CheckM'].map((h) => (
{h}
))}
{rest.map((m, i) => )}
{filtered.length > 86 && (
showing first 80 of {filtered.length - 6} remaining · use search and filters to narrow
)}
)}
);
}