papers.ts (4597B)
1 import { loadDashboard, loadPapersIndex, type PaperIndex } from '../data'; 2 import { navigate } from '../router'; 3 import { createFilters, updateFilterCount, type FilterState } from '../components/search-filter'; 4 import { renderSortableTable, type Column } from '../components/table'; 5 6 const DNA_CATS = [ 7 'artifacts', 'stat_method', 'eval_design', 8 'claims', 'setup', 'limitations', 9 'data_integrity', 'conflicts', 'contamination', 10 'human', 'cost', 11 ]; 12 13 const ENG_DIMS = [ 14 'practical', 'surprise', 'fear', 'drama', 'demo', 'brand', 15 ]; 16 17 function scoreColor(s: number): string { 18 if (s < 30) return 'var(--red)'; 19 if (s < 50) return 'var(--yellow)'; 20 if (s < 70) return 'var(--accent)'; 21 return 'var(--green)'; 22 } 23 24 function dnaCellColor(v: number | null): string { 25 if (v === null) return 'var(--border)'; 26 if (v < 25) return '#d03030'; 27 if (v < 50) return '#c08020'; 28 if (v < 75) return '#4070cc'; 29 return '#20a060'; 30 } 31 32 function engCellColor(v: number | null): string { 33 if (v === null) return 'var(--border)'; 34 if (v === 0) return '#555'; 35 if (v === 1) return '#7060a0'; 36 if (v === 2) return '#9070d0'; 37 return '#b080ff'; 38 } 39 40 function renderDna(dna: (number | null)[] | null, engagement: (number | null)[] | null): string { 41 if (!dna) return ''; 42 const methodCells = dna.map((v, i) => 43 `<span class="dna-cell" style="background:${dnaCellColor(v)}" title="${DNA_CATS[i]}: ${v !== null ? v + '%' : 'N/A'}"></span>` 44 ).join(''); 45 const engCells = engagement ? engagement.map((v, i) => 46 `<span class="dna-cell" style="background:${engCellColor(v)}" title="${ENG_DIMS[i]}: ${v !== null ? v + '/3' : '?'}"></span>` 47 ).join('') : ''; 48 return `<span class="dna-strip">${methodCells}</span>${engCells ? `<span class="dna-strip" style="margin-left:3px">${engCells}</span>` : ''}`; 49 } 50 51 export async function renderPapers(app: HTMLElement) { 52 app.innerHTML = '<div class="spinner"></div>'; 53 const [papers, dashboard] = await Promise.all([loadPapersIndex(), loadDashboard()]); 54 55 app.innerHTML = ''; 56 const archetypes = [...new Set(papers.map(p => p.archetype).filter((a): a is string => a != null))].sort(); 57 const tags = Object.keys(dashboard.tag_counts).sort(); 58 59 let filtered = [...papers]; 60 61 const filtersEl = createFilters(archetypes, tags, (state: FilterState) => { 62 filtered = papers.filter(p => { 63 if (state.search && !p.title.toLowerCase().includes(state.search)) return false; 64 if (state.year && p.year !== parseInt(state.year)) return false; 65 if (state.archetype && p.archetype !== state.archetype) return false; 66 if (state.tag && !p.tags.includes(state.tag)) return false; 67 if (p.score != null && (p.score < state.minScore || p.score > state.maxScore)) return false; 68 if (p.score == null && state.minScore > 0) return false; 69 return true; 70 }); 71 updateFilterCount(filtersEl, filtered.length, papers.length); 72 renderTable(); 73 }); 74 75 app.appendChild(filtersEl); 76 updateFilterCount(filtersEl, papers.length, papers.length); 77 78 const tableContainer = document.createElement('div'); 79 app.appendChild(tableContainer); 80 81 const columns: Column<PaperIndex>[] = [ 82 { key: 'title', label: 'Title', render: p => p.title.length > 60 ? p.title.slice(0, 57) + '...' : p.title, sortValue: p => p.title }, 83 { key: 'year', label: 'Year', render: p => String(p.year || ''), sortValue: p => p.year || 0 }, 84 { key: 'score', label: 'Score', render: p => { 85 if (p.score == null) return '<span style="color:var(--gray)">--</span>'; 86 if (p.paper_type === 'non-empirical') return `<span style="color:var(--gray)" title="Non-empirical paper — scored on limited criteria">${p.score}%*</span>`; 87 return `<span style="color:${scoreColor(p.score)}">${p.score}%</span>`; 88 }, sortValue: p => p.score ?? -1 }, 89 { key: 'dna', label: 'Profile', render: p => renderDna(p.dna, p.engagement), sortValue: p => p.score ?? -1 }, 90 { key: 'archetype', label: 'Type', render: p => { 91 if (p.paper_type === 'non-empirical') return '<span class="archetype" style="background:rgba(139,143,163,0.15);color:var(--text-dim)">Non-empirical</span>'; 92 return p.archetype ? `<span class="archetype ${p.archetype}">${p.archetype}</span>` : '<span style="color:var(--gray)">--</span>'; 93 }, sortValue: p => p.paper_type === 'non-empirical' ? 'ZZ' : (p.archetype || '') }, 94 ]; 95 96 function renderTable() { 97 tableContainer.innerHTML = ''; 98 const el = renderSortableTable(filtered, columns, p => { 99 if (p.score != null) navigate(`/paper/${p.id}`); 100 }); 101 tableContainer.appendChild(el); 102 } 103 104 renderTable(); 105 }