Mehdi commited on
Commit Β·
dbd761b
1
Parent(s): 867662d
feat: Field Notes blog as a standalone designed website (blog/index.html)
Browse filesEditorial dark glassmorphism matching the PaperProf brand: Inter +
Playfair Display pull quotes + JetBrains Mono, reading progress bar,
animated stat counters, attempt timeline, reveal-on-scroll, full
a11y (skip link, focus rings, reduced-motion, 4.5:1 contrast).
- blog/index.html +585 -0
blog/index.html
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>How We Fought Gradio, Won, and Built an AI Study Buddy in 10 Days β PaperProf Field Notes</title>
|
| 7 |
+
<meta name="description" content="Field notes from the Build Small Hackathon: building PaperProf, an AI study buddy running MiniCPM4-8B and FLUX.2-klein on ZeroGPU with a fully custom UI over a hidden-Gradio bridge.">
|
| 8 |
+
<meta property="og:title" content="PaperProf Field Notes β How We Fought Gradio and Won">
|
| 9 |
+
<meta property="og:description" content="68 commits, 10 days, zero external APIs. What we built and what we learned.">
|
| 10 |
+
<meta property="og:type" content="article">
|
| 11 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 12 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 13 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400&family=JetBrains+Mono:wght@400;500&family=Playfair+Display:ital,wght@1,400;1,500&display=swap" rel="stylesheet">
|
| 14 |
+
<style>
|
| 15 |
+
:root {
|
| 16 |
+
--bg: #0A0F1E;
|
| 17 |
+
--bg-card: rgba(255,255,255,0.03);
|
| 18 |
+
--bg-glass: rgba(10,15,30,0.65);
|
| 19 |
+
--border: rgba(255,255,255,0.08);
|
| 20 |
+
--border-hover: rgba(167,139,250,0.35);
|
| 21 |
+
--violet: #A78BFA;
|
| 22 |
+
--violet-deep: #7C3AED;
|
| 23 |
+
--cyan: #67E8F9;
|
| 24 |
+
--cyan-deep: #06B6D4;
|
| 25 |
+
--green: #6EE7B7;
|
| 26 |
+
--amber: #FCD34D;
|
| 27 |
+
--red: #FCA5A5;
|
| 28 |
+
--text: #E2E8F0;
|
| 29 |
+
--text-dim: #94A3B8;
|
| 30 |
+
--text-faint: #64748B;
|
| 31 |
+
--grad: linear-gradient(135deg, #A78BFA 0%, #67E8F9 100%);
|
| 32 |
+
--grad-deep: linear-gradient(135deg, #7C3AED, #06B6D4);
|
| 33 |
+
--font-ui: 'Inter', system-ui, sans-serif;
|
| 34 |
+
--font-quote: 'Playfair Display', Georgia, serif;
|
| 35 |
+
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
| 36 |
+
}
|
| 37 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 38 |
+
html { scroll-behavior: smooth; color-scheme: dark; }
|
| 39 |
+
@media (prefers-reduced-motion: reduce) {
|
| 40 |
+
html { scroll-behavior: auto; }
|
| 41 |
+
*, *::before, *::after { animation: none !important; transition: none !important; }
|
| 42 |
+
}
|
| 43 |
+
body {
|
| 44 |
+
background: var(--bg);
|
| 45 |
+
color: var(--text);
|
| 46 |
+
font-family: var(--font-ui);
|
| 47 |
+
font-size: 1.0625rem;
|
| 48 |
+
line-height: 1.75;
|
| 49 |
+
-webkit-font-smoothing: antialiased;
|
| 50 |
+
overflow-x: hidden;
|
| 51 |
+
}
|
| 52 |
+
::selection { background: rgba(124,58,237,0.45); color: #fff; }
|
| 53 |
+
|
| 54 |
+
/* Ambient background orbs */
|
| 55 |
+
.orb {
|
| 56 |
+
position: fixed; border-radius: 50%; filter: blur(120px);
|
| 57 |
+
pointer-events: none; z-index: 0; opacity: .5;
|
| 58 |
+
}
|
| 59 |
+
.orb-1 { width: 560px; height: 560px; background: rgba(124,58,237,0.18); top: -180px; left: -120px; }
|
| 60 |
+
.orb-2 { width: 480px; height: 480px; background: rgba(6,182,212,0.13); top: 40vh; right: -160px; }
|
| 61 |
+
.orb-3 { width: 520px; height: 520px; background: rgba(124,58,237,0.10); bottom: -200px; left: 30vw; }
|
| 62 |
+
|
| 63 |
+
/* Skip link */
|
| 64 |
+
.skip-link {
|
| 65 |
+
position: absolute; top: -48px; left: 16px; z-index: 100;
|
| 66 |
+
background: var(--violet-deep); color: #fff; padding: 10px 18px;
|
| 67 |
+
border-radius: 8px; font-weight: 600; text-decoration: none;
|
| 68 |
+
transition: top .2s;
|
| 69 |
+
}
|
| 70 |
+
.skip-link:focus { top: 12px; }
|
| 71 |
+
|
| 72 |
+
/* Reading progress */
|
| 73 |
+
#progress {
|
| 74 |
+
position: fixed; top: 0; left: 0; height: 3px; width: 0;
|
| 75 |
+
background: var(--grad-deep); z-index: 90;
|
| 76 |
+
box-shadow: 0 0 12px rgba(124,58,237,0.6);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/* Focus visibility */
|
| 80 |
+
a:focus-visible, button:focus-visible {
|
| 81 |
+
outline: 3px solid var(--cyan); outline-offset: 3px; border-radius: 4px;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* ββ Hero ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 85 |
+
.hero {
|
| 86 |
+
position: relative; z-index: 1;
|
| 87 |
+
max-width: 880px; margin: 0 auto;
|
| 88 |
+
padding: clamp(72px, 12vh, 140px) 24px 56px;
|
| 89 |
+
text-align: center;
|
| 90 |
+
}
|
| 91 |
+
.hero-badge {
|
| 92 |
+
display: inline-flex; align-items: center; gap: 8px;
|
| 93 |
+
font-family: var(--font-mono); font-size: .8rem; letter-spacing: 2.5px;
|
| 94 |
+
text-transform: uppercase; color: var(--cyan);
|
| 95 |
+
border: 1px solid rgba(103,232,249,0.25);
|
| 96 |
+
background: rgba(6,182,212,0.07);
|
| 97 |
+
padding: 8px 18px; border-radius: 50px; margin-bottom: 32px;
|
| 98 |
+
}
|
| 99 |
+
.hero h1 {
|
| 100 |
+
font-size: clamp(2.1rem, 5.4vw, 3.6rem);
|
| 101 |
+
font-weight: 800; line-height: 1.12; letter-spacing: -1.5px;
|
| 102 |
+
background: var(--grad);
|
| 103 |
+
-webkit-background-clip: text; background-clip: text;
|
| 104 |
+
-webkit-text-fill-color: transparent; color: var(--violet);
|
| 105 |
+
margin-bottom: 24px;
|
| 106 |
+
}
|
| 107 |
+
.hero-sub {
|
| 108 |
+
font-size: clamp(1.05rem, 2.2vw, 1.25rem);
|
| 109 |
+
color: var(--text-dim); max-width: 620px; margin: 0 auto 36px;
|
| 110 |
+
}
|
| 111 |
+
.hero-meta {
|
| 112 |
+
display: flex; flex-wrap: wrap; justify-content: center; gap: 14px 28px;
|
| 113 |
+
font-family: var(--font-mono); font-size: .82rem; color: var(--text-faint);
|
| 114 |
+
}
|
| 115 |
+
.hero-meta span { display: inline-flex; align-items: center; gap: 7px; }
|
| 116 |
+
.hero-meta svg { width: 15px; height: 15px; stroke: var(--violet); }
|
| 117 |
+
|
| 118 |
+
/* Stats band */
|
| 119 |
+
.stats {
|
| 120 |
+
position: relative; z-index: 1;
|
| 121 |
+
max-width: 880px; margin: 24px auto 0; padding: 0 24px;
|
| 122 |
+
display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px;
|
| 123 |
+
}
|
| 124 |
+
.stat {
|
| 125 |
+
background: var(--bg-glass); border: 1px solid var(--border);
|
| 126 |
+
border-radius: 16px; padding: 22px 12px; text-align: center;
|
| 127 |
+
backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
|
| 128 |
+
transition: transform .25s, border-color .25s;
|
| 129 |
+
}
|
| 130 |
+
.stat:hover { transform: translateY(-3px); border-color: var(--border-hover); }
|
| 131 |
+
.stat-num {
|
| 132 |
+
display: block; font-family: var(--font-mono); font-size: clamp(1.6rem, 3.4vw, 2.2rem);
|
| 133 |
+
font-weight: 500;
|
| 134 |
+
background: var(--grad); -webkit-background-clip: text; background-clip: text;
|
| 135 |
+
-webkit-text-fill-color: transparent; color: var(--violet);
|
| 136 |
+
}
|
| 137 |
+
.stat-label {
|
| 138 |
+
font-size: .78rem; letter-spacing: 1.5px; text-transform: uppercase;
|
| 139 |
+
color: var(--text-faint); margin-top: 6px; display: block;
|
| 140 |
+
}
|
| 141 |
+
@media (max-width: 640px) { .stats { grid-template-columns: repeat(2, 1fr); } }
|
| 142 |
+
|
| 143 |
+
/* ββ Article βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 144 |
+
main {
|
| 145 |
+
position: relative; z-index: 1;
|
| 146 |
+
max-width: 720px; margin: 0 auto; padding: 72px 24px 40px;
|
| 147 |
+
}
|
| 148 |
+
main > * + * { margin-top: 1.5em; }
|
| 149 |
+
h2 {
|
| 150 |
+
font-size: clamp(1.55rem, 3.4vw, 2.05rem); font-weight: 700;
|
| 151 |
+
letter-spacing: -0.5px; line-height: 1.25;
|
| 152 |
+
margin-top: 2.6em !important; scroll-margin-top: 90px;
|
| 153 |
+
}
|
| 154 |
+
h2 .h-accent {
|
| 155 |
+
background: var(--grad); -webkit-background-clip: text; background-clip: text;
|
| 156 |
+
-webkit-text-fill-color: transparent; color: var(--violet);
|
| 157 |
+
}
|
| 158 |
+
h3 {
|
| 159 |
+
font-size: 1.22rem; font-weight: 700; color: var(--violet);
|
| 160 |
+
margin-top: 2.2em !important; scroll-margin-top: 90px;
|
| 161 |
+
}
|
| 162 |
+
p { color: var(--text); }
|
| 163 |
+
p strong { color: #fff; }
|
| 164 |
+
a { color: var(--cyan); text-decoration: underline; text-decoration-color: rgba(103,232,249,0.35); text-underline-offset: 3px; transition: text-decoration-color .2s; }
|
| 165 |
+
a:hover { text-decoration-color: var(--cyan); }
|
| 166 |
+
em { color: var(--text-dim); }
|
| 167 |
+
|
| 168 |
+
.lede {
|
| 169 |
+
font-size: 1.22rem; line-height: 1.8; color: var(--text-dim);
|
| 170 |
+
}
|
| 171 |
+
.lede::first-letter {
|
| 172 |
+
font-family: var(--font-quote); font-style: italic;
|
| 173 |
+
font-size: 3.4em; float: left; line-height: .82;
|
| 174 |
+
padding: 6px 12px 0 0; color: var(--violet);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
/* Pull quote */
|
| 178 |
+
blockquote {
|
| 179 |
+
font-family: var(--font-quote); font-style: italic;
|
| 180 |
+
font-size: clamp(1.3rem, 2.8vw, 1.6rem); line-height: 1.55;
|
| 181 |
+
color: #fff; text-align: center;
|
| 182 |
+
padding: 40px 24px; margin: 2.4em 0 !important;
|
| 183 |
+
border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
|
| 184 |
+
position: relative;
|
| 185 |
+
}
|
| 186 |
+
blockquote::before {
|
| 187 |
+
content: '"'; position: absolute; top: -4px; left: 50%; transform: translateX(-50%);
|
| 188 |
+
background: var(--bg); padding: 0 18px;
|
| 189 |
+
font-size: 2.6rem; color: var(--violet); font-style: normal; line-height: 1;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
/* Code blocks */
|
| 193 |
+
pre {
|
| 194 |
+
background: #0D1326; border: 1px solid var(--border);
|
| 195 |
+
border-radius: 14px; padding: 22px 24px; overflow-x: auto;
|
| 196 |
+
font-family: var(--font-mono); font-size: .86rem; line-height: 1.65;
|
| 197 |
+
position: relative; margin: 1.8em 0 !important;
|
| 198 |
+
}
|
| 199 |
+
pre::before {
|
| 200 |
+
content: attr(data-lang);
|
| 201 |
+
position: absolute; top: 10px; right: 16px;
|
| 202 |
+
font-size: .68rem; letter-spacing: 1.5px; text-transform: uppercase;
|
| 203 |
+
color: var(--text-faint);
|
| 204 |
+
}
|
| 205 |
+
code { font-family: var(--font-mono); }
|
| 206 |
+
p code, li code {
|
| 207 |
+
background: rgba(124,58,237,0.12); border: 1px solid rgba(124,58,237,0.2);
|
| 208 |
+
padding: 2px 7px; border-radius: 6px; font-size: .85em; color: var(--violet);
|
| 209 |
+
white-space: nowrap;
|
| 210 |
+
}
|
| 211 |
+
.tok-c { color: var(--text-faint); font-style: italic; }
|
| 212 |
+
.tok-k { color: var(--violet); }
|
| 213 |
+
.tok-s { color: var(--green); }
|
| 214 |
+
.tok-f { color: var(--cyan); }
|
| 215 |
+
.tok-n { color: var(--amber); }
|
| 216 |
+
|
| 217 |
+
/* Pipeline diagram */
|
| 218 |
+
.pipeline {
|
| 219 |
+
background: var(--bg-glass); border: 1px solid var(--border);
|
| 220 |
+
border-radius: 16px; padding: 28px 22px; margin: 2em 0 !important;
|
| 221 |
+
backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
|
| 222 |
+
display: flex; flex-direction: column; gap: 0; align-items: center;
|
| 223 |
+
}
|
| 224 |
+
.pipe-step {
|
| 225 |
+
display: flex; align-items: center; gap: 14px;
|
| 226 |
+
background: var(--bg-card); border: 1px solid var(--border);
|
| 227 |
+
border-radius: 12px; padding: 13px 22px; width: 100%; max-width: 460px;
|
| 228 |
+
transition: border-color .25s, transform .25s;
|
| 229 |
+
}
|
| 230 |
+
.pipe-step:hover { border-color: var(--border-hover); transform: scale(1.02); }
|
| 231 |
+
.pipe-step .ico { font-size: 1.3rem; flex-shrink: 0; }
|
| 232 |
+
.pipe-step .lbl { font-weight: 600; color: #fff; font-size: .95rem; }
|
| 233 |
+
.pipe-step .sub { font-family: var(--font-mono); font-size: .76rem; color: var(--text-faint); display: block; }
|
| 234 |
+
.pipe-arrow { color: var(--violet); font-size: 1.1rem; padding: 7px 0; line-height: 1; }
|
| 235 |
+
|
| 236 |
+
/* Feature & lesson cards */
|
| 237 |
+
.cards { display: grid; gap: 16px; margin: 2em 0 !important; }
|
| 238 |
+
.cards.two { grid-template-columns: repeat(2, 1fr); }
|
| 239 |
+
@media (max-width: 600px) { .cards.two { grid-template-columns: 1fr; } }
|
| 240 |
+
.card {
|
| 241 |
+
background: var(--bg-glass); border: 1px solid var(--border);
|
| 242 |
+
border-radius: 16px; padding: 26px;
|
| 243 |
+
backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
|
| 244 |
+
transition: transform .25s, border-color .25s, box-shadow .25s;
|
| 245 |
+
}
|
| 246 |
+
.card:hover {
|
| 247 |
+
transform: translateY(-4px); border-color: var(--border-hover);
|
| 248 |
+
box-shadow: 0 12px 40px rgba(124,58,237,0.12);
|
| 249 |
+
}
|
| 250 |
+
.card .ico { font-size: 1.7rem; display: block; margin-bottom: 12px; }
|
| 251 |
+
.card h4 { font-size: 1.04rem; font-weight: 700; color: #fff; margin-bottom: 8px; }
|
| 252 |
+
.card p { font-size: .93rem; color: var(--text-dim); line-height: 1.65; }
|
| 253 |
+
.card p code { white-space: normal; }
|
| 254 |
+
|
| 255 |
+
/* Numbered lesson headers */
|
| 256 |
+
.lesson-num {
|
| 257 |
+
display: inline-flex; align-items: center; justify-content: center;
|
| 258 |
+
width: 40px; height: 40px; border-radius: 12px; margin-right: 14px;
|
| 259 |
+
background: var(--grad-deep); color: #fff;
|
| 260 |
+
font-family: var(--font-mono); font-size: 1.05rem; font-weight: 500;
|
| 261 |
+
vertical-align: middle; flex-shrink: 0;
|
| 262 |
+
box-shadow: 0 4px 16px rgba(124,58,237,0.35);
|
| 263 |
+
}
|
| 264 |
+
.lesson-title { display: flex; align-items: center; }
|
| 265 |
+
|
| 266 |
+
/* Timeline */
|
| 267 |
+
.timeline { margin: 2em 0 !important; padding-left: 8px; }
|
| 268 |
+
.tl-item {
|
| 269 |
+
position: relative; padding: 0 0 28px 36px;
|
| 270 |
+
border-left: 2px solid rgba(124,58,237,0.25);
|
| 271 |
+
}
|
| 272 |
+
.tl-item:last-child { border-left-color: transparent; padding-bottom: 0; }
|
| 273 |
+
.tl-item::before {
|
| 274 |
+
content: ''; position: absolute; left: -8px; top: 4px;
|
| 275 |
+
width: 14px; height: 14px; border-radius: 50%;
|
| 276 |
+
background: var(--bg); border: 3px solid var(--violet-deep);
|
| 277 |
+
}
|
| 278 |
+
.tl-item.fail::before { border-color: var(--red); }
|
| 279 |
+
.tl-item.win::before { border-color: var(--green); box-shadow: 0 0 12px rgba(110,231,183,0.5); }
|
| 280 |
+
.tl-tag {
|
| 281 |
+
font-family: var(--font-mono); font-size: .72rem; letter-spacing: 1.5px;
|
| 282 |
+
text-transform: uppercase; margin-bottom: 4px; display: block;
|
| 283 |
+
}
|
| 284 |
+
.tl-item.fail .tl-tag { color: var(--red); }
|
| 285 |
+
.tl-item.win .tl-tag { color: var(--green); }
|
| 286 |
+
.tl-item h4 { color: #fff; font-size: 1.05rem; margin-bottom: 6px; }
|
| 287 |
+
.tl-item p { font-size: .94rem; color: var(--text-dim); }
|
| 288 |
+
|
| 289 |
+
/* Commit chip */
|
| 290 |
+
.commit {
|
| 291 |
+
font-family: var(--font-mono); font-size: .8rem;
|
| 292 |
+
background: rgba(110,231,183,0.07); border: 1px solid rgba(110,231,183,0.2);
|
| 293 |
+
color: var(--green); border-radius: 8px; padding: 10px 16px;
|
| 294 |
+
display: block; overflow-x: auto; white-space: nowrap; margin: 1.4em 0 !important;
|
| 295 |
+
}
|
| 296 |
+
.commit::before { content: 'β git log '; color: var(--text-faint); }
|
| 297 |
+
|
| 298 |
+
/* Table */
|
| 299 |
+
.tbl-wrap { overflow-x: auto; margin: 2em 0 !important; border-radius: 16px; border: 1px solid var(--border); }
|
| 300 |
+
table { width: 100%; border-collapse: collapse; background: var(--bg-glass); font-size: .94rem; }
|
| 301 |
+
th {
|
| 302 |
+
text-align: left; font-family: var(--font-mono); font-size: .72rem;
|
| 303 |
+
letter-spacing: 1.5px; text-transform: uppercase; color: var(--violet);
|
| 304 |
+
padding: 16px 20px; border-bottom: 1px solid var(--border);
|
| 305 |
+
}
|
| 306 |
+
td { padding: 14px 20px; border-bottom: 1px solid rgba(255,255,255,0.04); color: var(--text-dim); }
|
| 307 |
+
tr:last-child td { border-bottom: none; }
|
| 308 |
+
td strong { color: #fff; font-weight: 600; }
|
| 309 |
+
|
| 310 |
+
/* Takeaways list */
|
| 311 |
+
ol.takeaways { list-style: none; counter-reset: tk; margin: 2em 0 !important; display: grid; gap: 14px; }
|
| 312 |
+
ol.takeaways li {
|
| 313 |
+
counter-increment: tk; position: relative;
|
| 314 |
+
background: var(--bg-glass); border: 1px solid var(--border);
|
| 315 |
+
border-radius: 14px; padding: 20px 22px 20px 66px;
|
| 316 |
+
transition: border-color .25s;
|
| 317 |
+
}
|
| 318 |
+
ol.takeaways li:hover { border-color: var(--border-hover); }
|
| 319 |
+
ol.takeaways li::before {
|
| 320 |
+
content: counter(tk);
|
| 321 |
+
position: absolute; left: 18px; top: 20px;
|
| 322 |
+
width: 32px; height: 32px; border-radius: 10px;
|
| 323 |
+
display: flex; align-items: center; justify-content: center;
|
| 324 |
+
background: rgba(124,58,237,0.15); border: 1px solid rgba(124,58,237,0.3);
|
| 325 |
+
color: var(--violet); font-family: var(--font-mono); font-size: .9rem;
|
| 326 |
+
}
|
| 327 |
+
ol.takeaways strong { color: #fff; }
|
| 328 |
+
ol.takeaways li { color: var(--text-dim); font-size: .97rem; }
|
| 329 |
+
|
| 330 |
+
/* Reveal on scroll */
|
| 331 |
+
.reveal { opacity: 0; transform: translateY(24px); transition: opacity .6s ease, transform .6s ease; }
|
| 332 |
+
.reveal.in { opacity: 1; transform: none; }
|
| 333 |
+
@media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; transform: none; } }
|
| 334 |
+
|
| 335 |
+
/* ββ CTA footer ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 336 |
+
.cta {
|
| 337 |
+
position: relative; z-index: 1;
|
| 338 |
+
max-width: 720px; margin: 80px auto 0; padding: 0 24px;
|
| 339 |
+
}
|
| 340 |
+
.cta-card {
|
| 341 |
+
background: var(--bg-glass); border: 1px solid var(--border);
|
| 342 |
+
border-radius: 24px; padding: clamp(36px, 6vw, 60px); text-align: center;
|
| 343 |
+
backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
|
| 344 |
+
position: relative; overflow: hidden;
|
| 345 |
+
}
|
| 346 |
+
.cta-card::before {
|
| 347 |
+
content: ''; position: absolute; inset: 0;
|
| 348 |
+
background: radial-gradient(circle at 50% -20%, rgba(124,58,237,0.25), transparent 60%);
|
| 349 |
+
pointer-events: none;
|
| 350 |
+
}
|
| 351 |
+
.cta h2 { margin-top: 0 !important; font-size: clamp(1.5rem, 3.6vw, 2rem); }
|
| 352 |
+
.cta p { color: var(--text-dim); margin: 14px auto 30px; max-width: 440px; }
|
| 353 |
+
.btn {
|
| 354 |
+
display: inline-block; background: var(--grad-deep); color: #fff !important;
|
| 355 |
+
font-weight: 700; font-size: 1.04rem; letter-spacing: .3px;
|
| 356 |
+
padding: 16px 38px; border-radius: 50px; text-decoration: none;
|
| 357 |
+
box-shadow: 0 4px 24px rgba(124,58,237,0.4);
|
| 358 |
+
transition: transform .2s, box-shadow .2s;
|
| 359 |
+
}
|
| 360 |
+
.btn:hover { transform: translateY(-2px); box-shadow: 0 8px 32px rgba(124,58,237,0.55); }
|
| 361 |
+
|
| 362 |
+
footer {
|
| 363 |
+
position: relative; z-index: 1;
|
| 364 |
+
text-align: center; padding: 56px 24px 40px;
|
| 365 |
+
color: var(--text-faint); font-size: .85rem;
|
| 366 |
+
}
|
| 367 |
+
footer .mono { font-family: var(--font-mono); font-size: .75rem; letter-spacing: 1px; margin-top: 8px; display: block; }
|
| 368 |
+
</style>
|
| 369 |
+
</head>
|
| 370 |
+
<body>
|
| 371 |
+
|
| 372 |
+
<a class="skip-link" href="#article">Skip to article</a>
|
| 373 |
+
<div id="progress" role="presentation"></div>
|
| 374 |
+
<div class="orb orb-1"></div><div class="orb orb-2"></div><div class="orb orb-3"></div>
|
| 375 |
+
|
| 376 |
+
<header class="hero">
|
| 377 |
+
<span class="hero-badge">π Field Notes Β· Build Small Hackathon</span>
|
| 378 |
+
<h1>How We Fought Gradio, Won, and Built an AI Study Buddy in 10 Days</h1>
|
| 379 |
+
<p class="hero-sub">PaperProf turns any course PDF into a personal professor β running entirely on open-weight models, free GPUs, and stubbornness.</p>
|
| 380 |
+
<div class="hero-meta">
|
| 381 |
+
<span><svg fill="none" viewBox="0 0 24 24" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>June 2026</span>
|
| 382 |
+
<span><svg fill="none" viewBox="0 0 24 24" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>9 min read</span>
|
| 383 |
+
<span><svg fill="none" viewBox="0 0 24 24" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>EPITA Β· Team PaperProf</span>
|
| 384 |
+
</div>
|
| 385 |
+
</header>
|
| 386 |
+
|
| 387 |
+
<section class="stats" aria-label="Project statistics">
|
| 388 |
+
<div class="stat reveal"><span class="stat-num" data-count="68">0</span><span class="stat-label">commits</span></div>
|
| 389 |
+
<div class="stat reveal"><span class="stat-num" data-count="10">0</span><span class="stat-label">days</span></div>
|
| 390 |
+
<div class="stat reveal"><span class="stat-num" data-count="2">0</span><span class="stat-label">AI models</span></div>
|
| 391 |
+
<div class="stat reveal"><span class="stat-num" data-count="0">0</span><span class="stat-label">external APIs</span></div>
|
| 392 |
+
</section>
|
| 393 |
+
|
| 394 |
+
<main id="article">
|
| 395 |
+
|
| 396 |
+
<p class="lede reveal">Every student knows the ritual: it's 11 PM, the exam is tomorrow, and you're re-reading the same lecture PDF for the fourth time, <em>feeling</em> productive while learning absolutely nothing. Passive re-reading is one of the worst-performing study techniques in the learning-science literature. Active recall β forcing yourself to answer questions β is one of the best.</p>
|
| 397 |
+
|
| 398 |
+
<p class="reveal">So we built <strong>PaperProf</strong>: drop in any course PDF, and it becomes your personal professor. It reads the material, generates exam-style questions from it, grades your answers like a patient tutor, and even paints you a parting gift when you finish your session. Everything runs on free infrastructure with <strong>zero external API calls</strong> β no OpenAI key, no rate limits, no data leaving the machine.</p>
|
| 399 |
+
|
| 400 |
+
<h2 class="reveal" id="what-it-does">What it <span class="h-accent">does</span></h2>
|
| 401 |
+
|
| 402 |
+
<div class="pipeline reveal" role="img" aria-label="PaperProf pipeline: PDF upload, text extraction, chunking, question generation, student answer, LLM evaluation, session image">
|
| 403 |
+
<div class="pipe-step"><span class="ico">π</span><div><span class="lbl">PDF upload</span><span class="sub">lecture notes Β· textbook Β· slides</span></div></div>
|
| 404 |
+
<div class="pipe-arrow">βΌ</div>
|
| 405 |
+
<div class="pipe-step"><span class="ico">π</span><div><span class="lbl">Parse & chunk</span><span class="sub">parser.py + chunker.py β PyMuPDF, thematic sections</span></div></div>
|
| 406 |
+
<div class="pipe-arrow">βΌ</div>
|
| 407 |
+
<div class="pipe-step"><span class="ico">π§ </span><div><span class="lbl">Question generation</span><span class="sub">questioner.py β MiniCPM4-8B writes ONE focused question</span></div></div>
|
| 408 |
+
<div class="pipe-arrow">βΌ</div>
|
| 409 |
+
<div class="pipe-step"><span class="ico">βοΈ</span><div><span class="lbl">You answer</span><span class="sub">open response or MCQ</span></div></div>
|
| 410 |
+
<div class="pipe-arrow">βΌ</div>
|
| 411 |
+
<div class="pipe-step"><span class="ico">π</span><div><span class="lbl">Tutor feedback</span><span class="sub">evaluator.py β verdict Β· strengths Β· gaps Β· model answer</span></div></div>
|
| 412 |
+
<div class="pipe-arrow">βΌ</div>
|
| 413 |
+
<div class="pipe-step"><span class="ico">π¨</span><div><span class="lbl">Session image</span><span class="sub">image_gen.py β FLUX.2-klein paints your topics</span></div></div>
|
| 414 |
+
</div>
|
| 415 |
+
|
| 416 |
+
<div class="cards two reveal">
|
| 417 |
+
<div class="card"><span class="ico">π¬</span><h4>Open questions</h4><p>Write a free-form answer and get structured tutor feedback: a verdict, what you got right, what you missed, and a model answer.</p></div>
|
| 418 |
+
<div class="card"><span class="ico">π</span><h4>MCQ mode</h4><p>Four plausible options, instant client-side grading, and a one-sentence explanation for <em>every</em> choice β not just the right one.</p></div>
|
| 419 |
+
<div class="card"><span class="ico">π</span><h4>Live score ring</h4><p>An animated SVG arc tracks your session in real time and shifts color with your accuracy.</p></div>
|
| 420 |
+
<div class="card"><span class="ico">πΌοΈ</span><h4>Visual reward</h4><p>End the session and FLUX.2-klein generates a unique image inspired by the topics you just studied.</p></div>
|
| 421 |
+
</div>
|
| 422 |
+
|
| 423 |
+
<h2 class="reveal" id="real-story">The real story: <span class="h-accent">68 commits of lessons</span></h2>
|
| 424 |
+
|
| 425 |
+
<p class="reveal">A hackathon README tells you what was built. The git log tells you what actually happened. Ours has 68 commits, and roughly two-thirds of them start with <code>fix:</code>. Here is the honest version.</p>
|
| 426 |
+
|
| 427 |
+
<h3 class="reveal lesson-title" id="lesson-1"><span class="lesson-num">1</span>Model choice is a compatibility problem, not a benchmark problem</h3>
|
| 428 |
+
|
| 429 |
+
<p class="reveal">We started with MiniCPM3-4B, upgraded to MiniCPM4-8B for better reasoning, and immediately hit the classic open-model trap: the model card says one thing, the <code>transformers</code> version on your machine says another.</p>
|
| 430 |
+
|
| 431 |
+
<span class="commit reveal">fix: pin transformers==4.57.1 for MiniCPM4-8B compatibility</span>
|
| 432 |
+
|
| 433 |
+
<p class="reveal">The follow-up lesson came from quantization: bitsandbytes 4-bit is great on a 16 GB local GPU and <em>completely unnecessary</em> on ZeroGPU's hardware β so we made it conditional:</p>
|
| 434 |
+
|
| 435 |
+
<pre class="reveal" data-lang="python"><code><span class="tok-c"># HF Spaces (ZeroGPU): skip quantization, use bfloat16 directly</span>
|
| 436 |
+
<span class="tok-k">if</span> os.environ.get(<span class="tok-s">"SPACE_ID"</span>):
|
| 437 |
+
<span class="tok-k">return</span> <span class="tok-k">None</span>
|
| 438 |
+
<span class="tok-c"># Locally: 4-bit when VRAM < 17 GB</span></code></pre>
|
| 439 |
+
|
| 440 |
+
<p class="reveal">Same code, two deployment targets, zero config files. Detect the environment, adapt.</p>
|
| 441 |
+
|
| 442 |
+
<h3 class="reveal lesson-title" id="lesson-2"><span class="lesson-num">2</span>The Off-Brand badge nearly broke us (and taught us the most)</h3>
|
| 443 |
+
|
| 444 |
+
<p class="reveal">The hackathon has an <strong>Off-Brand</strong> badge: ship a UI that doesn't look like the framework you built it with. We wanted PaperProf to look like a real product β glassmorphism, animated score ring, dark academia palette β not a Gradio demo.</p>
|
| 445 |
+
|
| 446 |
+
<div class="timeline reveal">
|
| 447 |
+
<div class="tl-item fail">
|
| 448 |
+
<span class="tl-tag">Attempt #1 β Failed</span>
|
| 449 |
+
<h4>Restyle Gradio with CSS</h4>
|
| 450 |
+
<p>Eleven consecutive commits of theme warfare. Gradio's theming always had one more <code>!important</code> than we did.</p>
|
| 451 |
+
</div>
|
| 452 |
+
<div class="tl-item fail">
|
| 453 |
+
<span class="tl-tag">Attempt #2 β Failed</span>
|
| 454 |
+
<h4>Nuke it from orbit: Docker + FastAPI</h4>
|
| 455 |
+
<p>Raw HTML served by FastAPI, Gradio relegated to a backend. Worked locally, died on Spaces β ZeroGPU only flows through the Gradio SDK.</p>
|
| 456 |
+
</div>
|
| 457 |
+
<div class="tl-item win">
|
| 458 |
+
<span class="tl-tag">Attempt #3 β Shipped π</span>
|
| 459 |
+
<h4>The hidden-component bridge</h4>
|
| 460 |
+
<p>Keep Gradio as an invisible backend <em>inside the page</em>. A fully custom HTML/CSS/JS interface in <code>gr.HTML</code>, every real Gradio component hidden off-screen, and a 300 ms polling loop ferrying data between the two worlds.</p>
|
| 461 |
+
</div>
|
| 462 |
+
</div>
|
| 463 |
+
|
| 464 |
+
<p class="reveal">This pattern produced the three hardest-won discoveries of the hackathon:</p>
|
| 465 |
+
|
| 466 |
+
<p class="reveal"><strong><code>display: none</code> silently kills Gradio.</strong> Components hidden that way never get their Svelte event handlers attached. The fix is the oldest trick in CSS:</p>
|
| 467 |
+
|
| 468 |
+
<pre class="reveal" data-lang="css"><code><span class="tok-c">/* collapsed but NOT display:none, so Gradio attaches handlers */</span>
|
| 469 |
+
<span class="tok-f">#hidden-row-question</span> { <span class="tok-n">height</span>: 0 !important; <span class="tok-n">overflow</span>: visible !important; }</code></pre>
|
| 470 |
+
|
| 471 |
+
<p class="reveal"><strong>You can't <code>.click()</code> a Gradio button from JS.</strong> Server-side rendering means the synthetic click goes nowhere. What <em>does</em> work: setting a hidden textbox's value through the native property descriptor, then dispatching events so Svelte notices:</p>
|
| 472 |
+
|
| 473 |
+
<pre class="reveal" data-lang="javascript"><code><span class="tok-k">function</span> <span class="tok-f">setGradioTA</span>(sel, val) {
|
| 474 |
+
<span class="tok-k">const</span> el = document.<span class="tok-f">querySelector</span>(sel);
|
| 475 |
+
Object.<span class="tok-f">getOwnPropertyDescriptor</span>(HTMLTextAreaElement.prototype, <span class="tok-s">'value'</span>)
|
| 476 |
+
.set.<span class="tok-f">call</span>(el, val);
|
| 477 |
+
el.<span class="tok-f">dispatchEvent</span>(<span class="tok-k">new</span> <span class="tok-f">Event</span>(<span class="tok-s">'input'</span>, {bubbles: <span class="tok-k">true</span>}));
|
| 478 |
+
el.<span class="tok-f">dispatchEvent</span>(<span class="tok-k">new</span> <span class="tok-f">Event</span>(<span class="tok-s">'change'</span>, {bubbles: <span class="tok-k">true</span>}));
|
| 479 |
+
}</code></pre>
|
| 480 |
+
|
| 481 |
+
<p class="reveal">Every action in PaperProf β generate question, submit answer, new MCQ β is a timestamp written into a hidden textbox, picked up by a <code>.change()</code> listener on the Python side. <em>Buttons that aren't buttons.</em></p>
|
| 482 |
+
|
| 483 |
+
<blockquote class="reveal">Sometimes the dumb solution is the senior solution.</blockquote>
|
| 484 |
+
|
| 485 |
+
<p class="reveal"><strong>MutationObserver loses to Svelte.</strong> Gradio's reactive DOM updates don't always fire observers the way you'd expect. We surrendered and switched to a humble <code>setInterval</code> polling loop. Less elegant, infinitely more reliable.</p>
|
| 486 |
+
|
| 487 |
+
<h3 class="reveal lesson-title" id="lesson-3"><span class="lesson-num">3</span>ZeroGPU makes you think in seconds</h3>
|
| 488 |
+
|
| 489 |
+
<p class="reveal">ZeroGPU gives you a serious GPU for free, but only in short decorated windows. That budget reshapes your architecture:</p>
|
| 490 |
+
|
| 491 |
+
<div class="cards reveal">
|
| 492 |
+
<div class="card"><span class="ico">π₯Ά</span><h4>Cold starts are real</h4><p>Loading an 8B model takes ~60β90s the first time. The UI is honest about it: a live elapsed-time counter, escalating messages, and a 3-minute hard timeout that unlocks the UI instead of spinning forever.</p></div>
|
| 493 |
+
<div class="card"><span class="ico">π¦</span><h4>Never download inside the GPU window</h4><p>FLUX.2-klein weighs ~16 GB. We prefetch it in a daemon thread at <em>startup</em>, so the <code>@spaces.GPU</code> window is spent generating, not downloading β and we skip a 7.75 GB duplicate checkpoint diffusers never reads.</p></div>
|
| 494 |
+
<div class="card"><span class="ico">β‘</span><h4>Don't burn GPU on what JS can do</h4><p>MCQ grading needs no model call. The LLM emits a structured format once, we parse it to JSON, and the browser grades clicks instantly. Zero latency, zero GPU seconds.</p></div>
|
| 495 |
+
</div>
|
| 496 |
+
|
| 497 |
+
<h3 class="reveal lesson-title" id="lesson-4"><span class="lesson-num">4</span>The bug that fired twice</h3>
|
| 498 |
+
|
| 499 |
+
<p class="reveal">Late in the hackathon, our session-summary modal showed every MCQ answer <strong>duplicated</strong>: answer one question, see it counted twice, score 0/2.</p>
|
| 500 |
+
|
| 501 |
+
<p class="reveal">The cause was textbook event-handling: MCQ buttons had <code>btn.onclick = handler</code> assigned in the display function <em>and</em> an <code>addEventListener</code> registered by the global wiring function. One click, two handlers, two score increments. Our first fix removed the wrong one β clicks then did <em>nothing at all</em>. The final fix kept the <code>onclick</code> (reassigned fresh with each question, inherently idempotent) and added a re-entrancy guard.</p>
|
| 502 |
+
|
| 503 |
+
<blockquote class="reveal">When two pieces of code both "helpfully" wire the same button, you don't have redundancy β you have a race.</blockquote>
|
| 504 |
+
|
| 505 |
+
<h3 class="reveal lesson-title" id="lesson-5"><span class="lesson-num">5</span>Prompts are product decisions</h3>
|
| 506 |
+
|
| 507 |
+
<p class="reveal">Small prompt details made the difference between "tech demo" and "usable study tool". Early questions were rambling multi-part monsters β the fix was brutal constraint: <em>"ONE question only, on ONE concept. Maximum 25 words. No sub-questions, no 'and'."</em> The evaluator follows a fixed 4-part structure so the frontend can parse and render it as styled sections β <strong>prompt format is API contract</strong>.</p>
|
| 508 |
+
|
| 509 |
+
<p class="reveal">And with French source PDFs, the model kept drifting into French. Polite instructions lost to the gravitational pull of the context. What finally worked: <code>IMPORTANT: Always write in English</code> β stated twice, top and bottom of the prompt. With 8B models, subtlety is wasted; repetition is a feature.</p>
|
| 510 |
+
|
| 511 |
+
<h2 class="reveal" id="takeaways">What we'd tell <span class="h-accent">past us</span></h2>
|
| 512 |
+
|
| 513 |
+
<ol class="takeaways reveal">
|
| 514 |
+
<li><strong>Read the git log of your own project.</strong> Two-thirds <code>fix:</code> commits isn't failure β it's the actual texture of shipping. Each one was a lesson nobody had written down for us.</li>
|
| 515 |
+
<li><strong>Frameworks fight back hardest at the edges.</strong> Using Gradio normally is easy. Using it as an invisible backend required understanding how it <em>actually</em> renders.</li>
|
| 516 |
+
<li><strong>Free infrastructure imposes honest engineering.</strong> No API credits to hide behind means caring about cold starts, GPU seconds, and weight prefetching. Constraints made the architecture better.</li>
|
| 517 |
+
<li><strong>Client-side everything you can.</strong> The MCQ mode is the snappiest feature in the app precisely because it never touches the server after generation.</li>
|
| 518 |
+
<li><strong>Ship the small thing.</strong> PaperProf does one loop β read, ask, grade, encourage β and does it end-to-end. A project that completes one circle beats one that sketches five.</li>
|
| 519 |
+
</ol>
|
| 520 |
+
|
| 521 |
+
<h2 class="reveal" id="stack">The <span class="h-accent">stack</span></h2>
|
| 522 |
+
|
| 523 |
+
<div class="tbl-wrap reveal">
|
| 524 |
+
<table>
|
| 525 |
+
<thead><tr><th scope="col">Layer</th><th scope="col">Choice</th></tr></thead>
|
| 526 |
+
<tbody>
|
| 527 |
+
<tr><td><strong>Q&A + evaluation</strong></td><td>MiniCPM4-8B (openbmb) Β· bfloat16 Β· transformers 4.57.1</td></tr>
|
| 528 |
+
<tr><td><strong>Session images</strong></td><td>FLUX.2-klein-4B (Black Forest Labs) Β· diffusers</td></tr>
|
| 529 |
+
<tr><td><strong>PDF parsing</strong></td><td>PyMuPDF</td></tr>
|
| 530 |
+
<tr><td><strong>Backend / hosting</strong></td><td>Gradio 6 on Hugging Face Spaces Β· ZeroGPU</td></tr>
|
| 531 |
+
<tr><td><strong>Frontend</strong></td><td>Hand-written HTML/CSS/JS over a hidden-Gradio bridge</td></tr>
|
| 532 |
+
<tr><td><strong>External APIs</strong></td><td><strong>None.</strong> π Fully off the grid.</td></tr>
|
| 533 |
+
</tbody>
|
| 534 |
+
</table>
|
| 535 |
+
</div>
|
| 536 |
+
|
| 537 |
+
</main>
|
| 538 |
+
|
| 539 |
+
<section class="cta">
|
| 540 |
+
<div class="cta-card reveal">
|
| 541 |
+
<h2>Bring a PDF.<br>Let the professor grill you. π</h2>
|
| 542 |
+
<p>The Space is live on Hugging Face β upload your course material and start an active-recall session in seconds.</p>
|
| 543 |
+
<a class="btn" href="https://huggingface.co/spaces/build-small-hackathon/PaperProf" target="_blank" rel="noopener">Try PaperProf live β</a>
|
| 544 |
+
</div>
|
| 545 |
+
</section>
|
| 546 |
+
|
| 547 |
+
<footer>
|
| 548 |
+
Built for the Build Small Hackathon Β· June 5β15, 2026
|
| 549 |
+
<span class="mono">MiniCPM4-8B Β· FLUX.2-klein Β· ZeroGPU Β· 0 external APIs</span>
|
| 550 |
+
</footer>
|
| 551 |
+
|
| 552 |
+
<script>
|
| 553 |
+
// Reading progress
|
| 554 |
+
const prog = document.getElementById('progress');
|
| 555 |
+
addEventListener('scroll', () => {
|
| 556 |
+
const h = document.documentElement;
|
| 557 |
+
prog.style.width = (h.scrollTop / (h.scrollHeight - h.clientHeight) * 100) + '%';
|
| 558 |
+
}, {passive: true});
|
| 559 |
+
|
| 560 |
+
// Reveal on scroll
|
| 561 |
+
const io = new IntersectionObserver(entries => {
|
| 562 |
+
for (const e of entries) if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); }
|
| 563 |
+
}, {threshold: 0.12, rootMargin: '0px 0px -40px 0px'});
|
| 564 |
+
document.querySelectorAll('.reveal').forEach(el => io.observe(el));
|
| 565 |
+
|
| 566 |
+
// Animated counters
|
| 567 |
+
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
| 568 |
+
const cio = new IntersectionObserver(entries => {
|
| 569 |
+
for (const e of entries) {
|
| 570 |
+
if (!e.isIntersecting) continue;
|
| 571 |
+
cio.unobserve(e.target);
|
| 572 |
+
const el = e.target, target = +el.dataset.count;
|
| 573 |
+
if (reduce || target === 0) { el.textContent = target; continue; }
|
| 574 |
+
const t0 = performance.now(), dur = 1100;
|
| 575 |
+
(function tick(now) {
|
| 576 |
+
const p = Math.min((now - t0) / dur, 1);
|
| 577 |
+
el.textContent = Math.round(target * (1 - Math.pow(1 - p, 3)));
|
| 578 |
+
if (p < 1) requestAnimationFrame(tick);
|
| 579 |
+
})(t0);
|
| 580 |
+
}
|
| 581 |
+
}, {threshold: 0.6});
|
| 582 |
+
document.querySelectorAll('.stat-num').forEach(el => cio.observe(el));
|
| 583 |
+
</script>
|
| 584 |
+
</body>
|
| 585 |
+
</html>
|