Mehdi commited on
Commit
dbd761b
Β·
1 Parent(s): 867662d

feat: Field Notes blog as a standalone designed website (blog/index.html)

Browse files

Editorial 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).

Files changed (1) hide show
  1. 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 &amp; 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&nbsp;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 &lt; 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&nbsp;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&amp;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>