// ════════════════════════════════════════════════════════════════ // ClusterDetailPage — drill-down при клике "Открыть стартапы кластера" // Контекст кластера + мини-аналитика + список стартапов // ════════════════════════════════════════════════════════════════ const { useMemo: useMemoCD, useState: useStateCD } = React; function ClusterDetailPage({ cluster, onBack, onOpenStartup }) { const D = window.SDH_DATA; const tintHue = cluster.momentum === 'rising' ? 'emerald' : cluster.momentum === 'cooling' ? 'rose' : 'info'; // Deterministic subset of corpus belonging to this cluster const clusterStartups = useMemoCD(() => { // seed from cluster.id like "t1" const seedNum = cluster.id.charCodeAt(1) * 7 + (cluster.id.length > 2 ? cluster.id.charCodeAt(2) : 0); const rng = (n) => ((seedNum * 9301 + n * 49297) % 233280) / 233280; // pick `cluster.count` startups from corpus by stable hash const ranked = D.corpus .map((s, i) => ({ s, key: rng(i) })) .sort((a, b) => a.key - b.key) .slice(0, cluster.count) .map(({ s }, i) => { // bias similarity higher in cluster return { ...s, similarity: Math.min(1, s.similarity * 0.5 + 0.45 + rng(i + 999) * 0.15) }; }); return ranked; }, [cluster.id, cluster.count]); // Stats const newThisWeek = clusterStartups.filter(s => s.daysAgo <= 7).length; const avgSim = clusterStartups.reduce((sum, s) => sum + s.similarity, 0) / Math.max(1, clusterStartups.length); const fundedCount = clusterStartups.filter(s => s.funding_usd).length; // Source distribution const sourceDist = useMemoCD(() => { const map = {}; clusterStartups.forEach(s => { map[s.source] = map[s.source] || { id: s.source, label: s.sourceLabel, hue: s.sourceHue, count: 0 }; map[s.source].count += 1; }); return Object.values(map).sort((a, b) => b.count - a.count); }, [clusterStartups]); // Geo distribution const geoDist = useMemoCD(() => { const map = {}; clusterStartups.forEach(s => { map[s.geo] = (map[s.geo] || 0) + 1; }); return Object.entries(map) .map(([geo, count]) => ({ geo, count })) .sort((a, b) => b.count - a.count) .slice(0, 6); }, [clusterStartups]); // Sort & filter state for the list const [sort, setSort] = useStateCD('sim'); const [verdict, setVerdict] = useStateCD('all'); // all | high | mid | low const filteredStartups = useMemoCD(() => { let arr = clusterStartups; if (verdict === 'high') arr = arr.filter(s => s.similarity >= 0.75); else if (verdict === 'mid') arr = arr.filter(s => s.similarity >= 0.5 && s.similarity < 0.75); else if (verdict === 'low') arr = arr.filter(s => s.similarity < 0.5); arr = arr.slice(); if (sort === 'sim') arr.sort((a, b) => b.similarity - a.similarity); else if (sort === 'date') arr.sort((a, b) => a.daysAgo - b.daysAgo); else if (sort === 'name') arr.sort((a, b) => a.name.localeCompare(b.name)); else if (sort === 'funded') arr.sort((a, b) => (b.funding_usd || 0) - (a.funding_usd || 0)); return arr; }, [clusterStartups, sort, verdict]); const high = clusterStartups.filter(s => s.similarity >= 0.75).length; const mid = clusterStartups.filter(s => s.similarity >= 0.5 && s.similarity < 0.75).length; const low = clusterStartups.filter(s => s.similarity < 0.5).length; return (
{cluster.recommendation || 'Сила сигнала растёт. Следующий шаг: прототип на трёх пользовательских сценариях.'}
В выборке нет стартапов с выбранными фильтрами.