/** Formatting and utility helpers */ export function formatNumber(n) { if (n == null) return '—'; if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M'; if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K'; return String(n); } export function formatBytes(bytes) { if (!bytes) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } export function formatDate(ts) { if (!ts) return '—'; return new Date(ts).toLocaleString(); } export function formatRelative(ts) { if (!ts) return '—'; const diff = Date.now() - new Date(ts).getTime(); const s = Math.floor(diff / 1000); if (s < 60) return 'just now'; const m = Math.floor(s / 60); if (m < 60) return `${m}m ago`; const h = Math.floor(m / 60); if (h < 24) return `${h}h ago`; const d = Math.floor(h / 24); return `${d}d ago`; } export function roleColor(role) { return { admin: 'red', faculty: 'yellow', student: 'blue' }[role] ?? 'gray'; } export function tierColor(tier) { return { GPU_NVIDIA: 'green', GPU_AMD: 'yellow', CPU_ONLY: 'gray', }[tier] ?? 'gray'; } export function modelTagColor(tag) { return { RECOMMENDED: 'green', POSSIBLE: 'yellow', NOT_RECOMMENDED: 'red', CPU_ONLY: 'gray', }[tag] ?? 'gray'; } /** Debounce a function */ export function debounce(fn, ms = 300) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }; } /** Copy text to clipboard and return a promise */ export async function copyToClipboard(text) { await navigator.clipboard.writeText(text); } /** Simple markdown-to-HTML for chat messages (code blocks, bold, italic, lists) */ export function renderMarkdown(text) { if (!text) return ''; let html = text // Code blocks .replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => `
${escapeHtml(code.trim())}`)
// Inline code
.replace(/`([^`]+)`/g, (_, c) => `${escapeHtml(c)}`)
// Bold
.replace(/\*\*(.*?)\*\*/g, '$1')
// Italic
.replace(/\*(.*?)\*/g, '$1')
// Headers
.replace(/^### (.+)$/gm, '')
// Single newlines
.replace(/\n/g, '
');
return `
${html}
`; } function escapeHtml(str) { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } /** Generate a heatmap grid for 26 weeks of activity data */ export function buildHeatmap(dailyData) { // dailyData: [{date: "2026-01-01", count: 5}, ...] const map = {}; dailyData.forEach(d => { map[d.date] = d.count; }); const today = new Date(); const cells = []; for (let i = 181; i >= 0; i--) { const d = new Date(today); d.setDate(d.getDate() - i); const key = d.toISOString().slice(0, 10); cells.push({ date: key, count: map[key] ?? 0 }); } return cells; } export function heatmapColor(count) { if (!count) return 'var(--surface3)'; if (count < 3) return 'rgba(217,116,73,0.25)'; if (count < 8) return 'rgba(217,116,73,0.50)'; if (count < 20) return 'rgba(217,116,73,0.75)'; return 'var(--accent)'; }