YANAbillionarie commited on
Commit
38d331b
Β·
verified Β·
1 Parent(s): 2f02bdf

Add app.py

Browse files
Files changed (1) hide show
  1. app.py +754 -0
app.py ADDED
@@ -0,0 +1,754 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ """
3
+ Submission Co-Pilot
4
+ ---------------------------------------------------------------
5
+ Three-page Gradio app for hackathon judges.
6
+ Page 1 – Hero / hook
7
+ Page 2 – Problem framing
8
+ Page 3 – The actual analyser tool
9
+
10
+ Drop app.py + requirements.txt into a Gradio HF Space. Done.
11
+ ---------------------------------------------------------------
12
+ """
13
+
14
+ import os
15
+ os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
16
+
17
+ import re
18
+ import gradio as gr
19
+ import pandas as pd
20
+ from huggingface_hub import HfApi, hf_hub_download
21
+
22
+ HF_TOKEN = os.environ.get("HF_TOKEN")
23
+ api = HfApi(token=HF_TOKEN)
24
+
25
+
26
+ # =============================================================================
27
+ # Styling
28
+ # =============================================================================
29
+ CUSTOM_CSS = """
30
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
31
+
32
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
33
+
34
+ body, .gradio-container {
35
+ background: #0d0d1a !important;
36
+ font-family: 'Inter', system-ui, sans-serif !important;
37
+ color: #e0e0f0 !important;
38
+ min-height: 100vh;
39
+ }
40
+
41
+ footer { display: none !important; }
42
+
43
+ /* ─── hero ─────────────────────────────────────── */
44
+ .hero-wrap {
45
+ min-height: 80vh;
46
+ display: flex;
47
+ flex-direction: column;
48
+ align-items: center;
49
+ justify-content: center;
50
+ text-align: center;
51
+ padding: 60px 24px;
52
+ }
53
+ .hero-badge {
54
+ display: inline-block;
55
+ background: rgba(245,166,35,0.15);
56
+ color: #F5A623;
57
+ border: 1px solid rgba(245,166,35,0.3);
58
+ border-radius: 100px;
59
+ padding: 6px 18px;
60
+ font-size: .75rem;
61
+ font-weight: 600;
62
+ letter-spacing: .08em;
63
+ text-transform: uppercase;
64
+ margin-bottom: 28px;
65
+ }
66
+ .hero-title {
67
+ font-size: clamp(2.2rem, 6vw, 4rem);
68
+ font-weight: 700;
69
+ line-height: 1.1;
70
+ color: #ffffff;
71
+ max-width: 700px;
72
+ margin-bottom: 20px;
73
+ }
74
+ .hero-title span { color: #F5A623; }
75
+ .hero-sub {
76
+ font-size: 1.05rem;
77
+ color: #8888a8;
78
+ max-width: 520px;
79
+ line-height: 1.7;
80
+ margin-bottom: 36px;
81
+ }
82
+ .bubble {
83
+ background: #1a1a30;
84
+ border: 1px solid #2e2e50;
85
+ border-radius: 16px 16px 16px 4px;
86
+ padding: 16px 22px;
87
+ font-size: .92rem;
88
+ color: #c0c0e0;
89
+ max-width: 420px;
90
+ margin: 0 auto 40px;
91
+ line-height: 1.6;
92
+ }
93
+
94
+ /* ─── problem page ─────────────────────────────── */
95
+ .problem-wrap {
96
+ max-width: 860px;
97
+ margin: 0 auto;
98
+ padding: 60px 24px;
99
+ text-align: center;
100
+ }
101
+ .section-title {
102
+ font-size: clamp(1.6rem, 4vw, 2.6rem);
103
+ font-weight: 700;
104
+ color: #ffffff;
105
+ margin-bottom: 12px;
106
+ }
107
+ .section-sub {
108
+ font-size: .95rem;
109
+ color: #8888a8;
110
+ margin-bottom: 40px;
111
+ line-height: 1.6;
112
+ }
113
+ .problem-grid {
114
+ display: grid;
115
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
116
+ gap: 16px;
117
+ margin-bottom: 40px;
118
+ text-align: left;
119
+ }
120
+ .p-card {
121
+ background: #12122a;
122
+ border: 1px solid #22224a;
123
+ border-radius: 12px;
124
+ padding: 20px;
125
+ }
126
+ .p-card .ico { font-size: 1.5rem; margin-bottom: 10px; display: block; }
127
+ .p-card h4 { font-size: .9rem; font-weight: 600; color: #e0e0f5; margin-bottom: 6px; }
128
+ .p-card p { font-size: .82rem; color: #7070a0; line-height: 1.55; }
129
+
130
+ /* ─── tool page ─────────────────────────────────── */
131
+ .tool-wrap {
132
+ max-width: 820px;
133
+ margin: 0 auto;
134
+ padding: 50px 24px 60px;
135
+ }
136
+ .tool-header { text-align: center; margin-bottom: 36px; }
137
+ .tool-card {
138
+ background: #12122a;
139
+ border: 1px solid #22224a;
140
+ border-radius: 16px;
141
+ padding: 32px;
142
+ margin-bottom: 24px;
143
+ }
144
+ .tool-note {
145
+ font-size: .8rem;
146
+ color: #5a5a80;
147
+ text-align: center;
148
+ margin-top: 14px;
149
+ padding-top: 14px;
150
+ border-top: 1px solid #22224a;
151
+ }
152
+
153
+ /* ─── buttons ───────────────────────────────────── */
154
+ .cta-btn {
155
+ background: #F5A623 !important;
156
+ color: #0d0d1a !important;
157
+ font-weight: 700 !important;
158
+ font-size: .95rem !important;
159
+ border: none !important;
160
+ border-radius: 100px !important;
161
+ padding: 12px 32px !important;
162
+ cursor: pointer !important;
163
+ transition: opacity .2s !important;
164
+ }
165
+ .cta-btn:hover { opacity: .85 !important; }
166
+
167
+ .ghost-btn {
168
+ background: transparent !important;
169
+ color: #8888a8 !important;
170
+ font-weight: 500 !important;
171
+ font-size: .9rem !important;
172
+ border: 1px solid #2e2e50 !important;
173
+ border-radius: 100px !important;
174
+ padding: 10px 24px !important;
175
+ cursor: pointer !important;
176
+ transition: border-color .2s !important;
177
+ }
178
+ .ghost-btn:hover { border-color: #F5A623 !important; }
179
+
180
+ .nav-row {
181
+ display: flex !important;
182
+ justify-content: center !important;
183
+ gap: 12px !important;
184
+ margin-top: 8px !important;
185
+ flex-wrap: wrap !important;
186
+ }
187
+
188
+ /* ─── footer ────────────────────────────────────── */
189
+ .site-footer {
190
+ text-align: center;
191
+ padding: 28px 16px;
192
+ font-size: .78rem;
193
+ color: #44446a;
194
+ border-top: 1px solid #181830;
195
+ margin-top: 16px;
196
+ }
197
+ """
198
+
199
+ # =============================================================================
200
+ # HTML page content
201
+ # =============================================================================
202
+ PAGE1_HTML = """
203
+ <div class="hero-wrap">
204
+ <span class="hero-badge">πŸ† Hackathon Judge's Co-Pilot</span>
205
+ <h1 class="hero-title">You're About To Judge<br><span>50+ Projects.</span></h1>
206
+ <p class="hero-sub">
207
+ The clock is running. The submissions keep coming.
208
+ Most tools leave you staring at raw READMEs.
209
+ This one does the reading for you.
210
+ </p>
211
+ <div class="bubble">
212
+ πŸ‘‹ Hey β€” we built this thinking about this exact moment.
213
+ Paste your links, set your criteria, and walk away with a
214
+ ranked briefing. No tab-switching required.
215
+ </div>
216
+ </div>
217
+ """
218
+
219
+ PAGE2_HTML = """
220
+ <div class="problem-wrap">
221
+ <h2 class="section-title">Judging Is Harder Than It Looks</h2>
222
+ <p class="section-sub">
223
+ Every year, judges face the same four walls.
224
+ We built this tool by starting with those walls.
225
+ </p>
226
+ <div class="problem-grid">
227
+ <div class="p-card">
228
+ <span class="ico">🌊</span>
229
+ <h4>Submission Overload</h4>
230
+ <p>Dozens of projects, one afternoon, zero shortcut β€” until now.</p>
231
+ </div>
232
+ <div class="p-card">
233
+ <span class="ico">🎯</span>
234
+ <h4>Inconsistent Criteria</h4>
235
+ <p>Different judges score different things. Rubrics drift mid-process.</p>
236
+ </div>
237
+ <div class="p-card">
238
+ <span class="ico">⏱️</span>
239
+ <h4>Time Per Submission</h4>
240
+ <p>Reading every README, checking every file β€” it adds up fast.</p>
241
+ </div>
242
+ <div class="p-card">
243
+ <span class="ico">🀯</span>
244
+ <h4>Decision Fatigue</h4>
245
+ <p>By submission 20, even great projects start blurring together.</p>
246
+ </div>
247
+ </div>
248
+ <div class="bubble">
249
+ The next page solves all four. One tool, your criteria, instant ranked results.
250
+ </div>
251
+ </div>
252
+ """
253
+
254
+ PAGE3_HEADER_HTML = """
255
+ <div class="tool-wrap">
256
+ <div class="tool-header">
257
+ <span class="hero-badge">✨ The Analyser</span>
258
+ <h2 class="section-title" style="margin-top:12px;margin-bottom:10px;">
259
+ Your Submission Briefing, Instant.
260
+ </h2>
261
+ <p class="section-sub">
262
+ Paste Space or model links, define what you are judging for,
263
+ and get a ranked leaderboard plus a written briefing in seconds.
264
+ </p>
265
+ </div>
266
+ </div>
267
+ """
268
+
269
+ FOOTER_HTML = """
270
+ <div class="site-footer">
271
+ Built with care for the Gradio x Hugging Face Hackathon 2026
272
+ &nbsp;&middot;&nbsp;
273
+ Automated first pass only β€” the final call is always yours.
274
+ </div>
275
+ """
276
+
277
+
278
+ # =============================================================================
279
+ # Scoring signal dictionaries
280
+ # =============================================================================
281
+ CRITERIA_SIGNALS = {
282
+ "innovation": {
283
+ "keywords": [
284
+ "novel", "new approach", "first", "unique", "creative", "original",
285
+ "invention", "breakthrough", "cutting-edge", "state-of-the-art",
286
+ ],
287
+ "checks": [],
288
+ },
289
+ "technical": {
290
+ "keywords": [
291
+ "architecture", "model", "training", "fine-tun", "dataset",
292
+ "benchmark", "parameter", "loss", "accuracy", "performance",
293
+ "evaluation", "metric",
294
+ ],
295
+ "checks": ["Has requirements.txt", "Has app entry point", "Uses Gradio"],
296
+ },
297
+ "usability": {
298
+ "keywords": [
299
+ "easy", "simple", "user", "interface", "demo", "interact",
300
+ "intuitive", "accessible", "tutorial", "guide", "how to",
301
+ ],
302
+ "checks": ["Has README", "Has app entry point"],
303
+ },
304
+ "design": {
305
+ "keywords": [
306
+ "design", "ui", "ux", "visual", "layout", "style", "theme",
307
+ "responsive", "mobile", "dark mode",
308
+ ],
309
+ "checks": ["Demo media"],
310
+ },
311
+ "impact": {
312
+ "keywords": [
313
+ "impact", "real-world", "problem", "solv", "benefit", "help",
314
+ "application", "use case", "deploy", "production", "society",
315
+ ],
316
+ "checks": [],
317
+ },
318
+ "presentation": {
319
+ "keywords": [
320
+ "demo", "video", "screenshot", "gif", "example", "showcase",
321
+ "show", "preview", "walkthrough",
322
+ ],
323
+ "checks": ["Demo media"],
324
+ },
325
+ "documentation": {
326
+ "keywords": [
327
+ "document", "readme", "explain", "describe", "usage", "install",
328
+ "setup", "guide", "instruction", "api",
329
+ ],
330
+ "checks": ["Has README", "Has LICENSE", "Has requirements.txt"],
331
+ },
332
+ "completeness": {
333
+ "keywords": [
334
+ "complete", "working", "functional", "deployed", "live",
335
+ "finish", "done", "ready",
336
+ ],
337
+ "checks": ["Has README", "Has requirements.txt", "Has app entry point", "Uses Gradio"],
338
+ },
339
+ }
340
+
341
+ CRITERIA_ALIASES = {
342
+ "innovation": "innovation",
343
+ "innovative": "innovation",
344
+ "originality": "innovation",
345
+ "technical": "technical",
346
+ "technical execution": "technical",
347
+ "code quality": "technical",
348
+ "engineering": "technical",
349
+ "usability": "usability",
350
+ "ux": "usability",
351
+ "user experience": "usability",
352
+ "design": "design",
353
+ "ui": "design",
354
+ "ui & design": "design",
355
+ "usability & design": "design",
356
+ "impact": "impact",
357
+ "real-world impact": "impact",
358
+ "presentation": "presentation",
359
+ "demo": "presentation",
360
+ "documentation": "documentation",
361
+ "docs": "documentation",
362
+ "completeness": "completeness",
363
+ "overall quality": "completeness",
364
+ }
365
+
366
+
367
+ # =============================================================================
368
+ # Hub helpers
369
+ # =============================================================================
370
+ def parse_hf_url(text: str):
371
+ """Return (repo_type, 'owner/name') or (None, None)."""
372
+ text = text.strip().rstrip("/")
373
+
374
+ for pattern, rtype in [
375
+ (r"huggingface\.co/spaces/([\w.\-]+/[\w.\-]+)", "space"),
376
+ (r"huggingface\.co/datasets/([\w.\-]+/[\w.\-]+)", "dataset"),
377
+ (r"huggingface\.co/([\w.\-]+/[\w.\-]+)", "model"),
378
+ ]:
379
+ m = re.search(pattern, text)
380
+ if m:
381
+ return rtype, m.group(1)
382
+
383
+ # Bare owner/name with no slashes beyond the one separator
384
+ m = re.fullmatch(r"([\w.\-]+)/([\w.\-]+)", text)
385
+ if m:
386
+ return "model", text
387
+
388
+ return None, None
389
+
390
+
391
+ def _get_repo_info(repo_type: str, repo_id: str):
392
+ """Use the type-specific HfApi methods (most stable API surface)."""
393
+ if repo_type == "space":
394
+ return api.space_info(repo_id)
395
+ if repo_type == "dataset":
396
+ return api.dataset_info(repo_id)
397
+ return api.model_info(repo_id)
398
+
399
+
400
+ def fetch_repo_data(repo_type: str, repo_id: str) -> dict:
401
+ data = {"readme": "", "files": [], "likes": 0, "error": ""}
402
+
403
+ try:
404
+ info = _get_repo_info(repo_type, repo_id)
405
+ data["likes"] = getattr(info, "likes", 0) or 0
406
+ siblings = getattr(info, "siblings", None) or []
407
+ data["files"] = [
408
+ s.rfilename for s in siblings if hasattr(s, "rfilename")
409
+ ]
410
+ except Exception as exc:
411
+ data["error"] = (
412
+ f"Could not reach this {repo_type} on the Hub "
413
+ f"({type(exc).__name__})."
414
+ )
415
+ return data
416
+
417
+ try:
418
+ path = hf_hub_download(
419
+ repo_id=repo_id,
420
+ filename="README.md",
421
+ repo_type=repo_type,
422
+ token=HF_TOKEN,
423
+ )
424
+ with open(path, encoding="utf-8", errors="replace") as fh:
425
+ data["readme"] = fh.read()
426
+ except Exception:
427
+ data["readme"] = "" # no README is fine; scoring will reflect it
428
+
429
+ return data
430
+
431
+
432
+ def fetch_app_code(repo_type: str, repo_id: str, files: list) -> str:
433
+ """Download app.py / main.py for Spaces only."""
434
+ if repo_type != "space":
435
+ return ""
436
+ entry = next(
437
+ (f for f in files if f.lower() in ("app.py", "main.py")),
438
+ None,
439
+ )
440
+ if not entry:
441
+ return ""
442
+ try:
443
+ path = hf_hub_download(
444
+ repo_id=repo_id,
445
+ filename=entry,
446
+ repo_type="space",
447
+ token=HF_TOKEN,
448
+ )
449
+ with open(path, encoding="utf-8", errors="replace") as fh:
450
+ return fh.read()
451
+ except Exception:
452
+ return ""
453
+
454
+
455
+ # =============================================================================
456
+ # Scoring
457
+ # =============================================================================
458
+ def _best_signal(criterion_lower: str):
459
+ return CRITERIA_SIGNALS.get(CRITERIA_ALIASES.get(criterion_lower))
460
+
461
+
462
+ def score_criterion(
463
+ criterion: str,
464
+ readme_lower: str,
465
+ checklist: dict,
466
+ repo_type: str,
467
+ ) -> float:
468
+ score = 5.0
469
+ crit_lower = criterion.lower().strip()
470
+ sig = _best_signal(crit_lower)
471
+
472
+ if sig:
473
+ kw_hits = sum(1 for kw in sig["keywords"] if kw in readme_lower)
474
+ score += min(kw_hits * 0.4, 3.0)
475
+ score += sum(0.7 for c in sig["checks"] if checklist.get(c))
476
+ else:
477
+ words = re.findall(r"[a-z]{4,}", crit_lower)
478
+ hits = sum(1 for w in words if w in readme_lower)
479
+ score += min(float(hits), 3.0)
480
+
481
+ if len(readme_lower) > 1200:
482
+ score += 0.5
483
+ elif len(readme_lower) < 120:
484
+ score -= 1.5
485
+
486
+ score += sum(0.2 for v in checklist.values() if v)
487
+
488
+ if repo_type == "space" and checklist.get("Uses Gradio"):
489
+ score += 0.3
490
+
491
+ return round(max(1.0, min(10.0, score)), 1)
492
+
493
+
494
+ def analyze_submission(raw: str, criteria_list: list) -> dict:
495
+ repo_type, repo_id = parse_hf_url(raw)
496
+
497
+ if not repo_id:
498
+ return {
499
+ "input": raw,
500
+ "title": raw,
501
+ "url": "",
502
+ "overall": 0,
503
+ "scores": {c: 0 for c in criteria_list},
504
+ "checklist":{},
505
+ "summary": "Could not parse as a Hugging Face link or 'owner/name'.",
506
+ "error": True,
507
+ "likes": 0,
508
+ }
509
+
510
+ data = fetch_repo_data(repo_type, repo_id)
511
+ prefix = {"space": "spaces/", "dataset": "datasets/", "model": ""}[repo_type]
512
+ url = f"https://huggingface.co/{prefix}{repo_id}"
513
+
514
+ if data["error"]:
515
+ return {
516
+ "input": raw,
517
+ "title": repo_id,
518
+ "url": url,
519
+ "overall": 0,
520
+ "scores": {c: 0 for c in criteria_list},
521
+ "checklist":{},
522
+ "summary": data["error"],
523
+ "error": True,
524
+ "likes": 0,
525
+ }
526
+
527
+ readme = data["readme"]
528
+ files = data["files"]
529
+ app_code = fetch_app_code(repo_type, repo_id, files)
530
+
531
+ # Strip YAML frontmatter (--- ... ---) from the top of the README
532
+ body = re.sub(r"^---.*?---", "", readme, count=1, flags=re.DOTALL).strip()
533
+
534
+ # Title: first H1 in the body (after frontmatter is removed)
535
+ title_match = re.search(r"^#\s+(.+)$", body, re.MULTILINE)
536
+ title = title_match.group(1).strip() if title_match else repo_id
537
+
538
+ # Summary: join non-header, non-badge lines
539
+ content_lines = []
540
+ for line in body.splitlines():
541
+ line = line.strip()
542
+ if not line:
543
+ continue
544
+ if line.startswith("#"):
545
+ continue
546
+ if line.startswith("![") or line.startswith("[!["):
547
+ continue
548
+ content_lines.append(line)
549
+ summary_raw = " ".join(content_lines)
550
+ summary = re.sub(r"\s+", " ", summary_raw)[:280] if content_lines else "No description found."
551
+
552
+ readme_lower = readme.lower()
553
+
554
+ checklist = {
555
+ "Has README": bool(readme.strip()),
556
+ "Has requirements.txt": any(f.lower() == "requirements.txt" for f in files),
557
+ "Has app entry point": any(f.lower() in ("app.py", "main.py") for f in files),
558
+ "Has LICENSE": any("license" in f.lower() for f in files),
559
+ "Uses Gradio": any(
560
+ s in app_code
561
+ for s in ("import gradio", "gr.Blocks",
562
+ "gr.Interface", "gr.ChatInterface")
563
+ ),
564
+ "Demo media": (
565
+ bool(re.search(r"\.(gif|mp4|png|jpe?g|webm)", readme, re.I))
566
+ or "youtube" in readme_lower
567
+ ),
568
+ }
569
+
570
+ scores = {
571
+ c.strip(): score_criterion(c.strip(), readme_lower, checklist, repo_type)
572
+ for c in criteria_list
573
+ if c.strip()
574
+ }
575
+ overall = round(sum(scores.values()) / len(scores), 1) if scores else 0
576
+
577
+ return {
578
+ "input": raw,
579
+ "title": title,
580
+ "url": url,
581
+ "overall": overall,
582
+ "scores": scores,
583
+ "checklist":checklist,
584
+ "summary": summary,
585
+ "error": False,
586
+ "likes": data["likes"],
587
+ }
588
+
589
+
590
+ # =============================================================================
591
+ # Entry point called by the Gradio button
592
+ # =============================================================================
593
+ def _run_analysis(urls_text: str, criteria_text: str):
594
+ if not urls_text or not urls_text.strip():
595
+ return (
596
+ "### πŸ‘€ Paste at least one submission link above to get started.",
597
+ pd.DataFrame(),
598
+ )
599
+
600
+ criteria_list = [
601
+ c.strip()
602
+ for c in re.split(r"[,\n]", criteria_text or "")
603
+ if c.strip()
604
+ ]
605
+ if not criteria_list:
606
+ criteria_list = ["Overall Quality"]
607
+
608
+ seen, targets = set(), []
609
+ for line in re.split(r"[\n,]", urls_text):
610
+ line = line.strip()
611
+ if line and line not in seen:
612
+ seen.add(line)
613
+ targets.append(line)
614
+
615
+ results = [analyze_submission(t, criteria_list) for t in targets]
616
+ results.sort(key=lambda r: r["overall"], reverse=True)
617
+
618
+ # ── Leaderboard DataFrame ────────────────────────────────────────────────
619
+ columns = ["Rank", "Submission", "Overall"] + criteria_list + ["Likes", "Notes"]
620
+ rows = []
621
+ for i, r in enumerate(results, start=1):
622
+ row = [i, r["title"], r["overall"]]
623
+ row += [r["scores"].get(c, "-") for c in criteria_list]
624
+ row.append(r["likes"])
625
+ if r["error"]:
626
+ row.append(r["summary"])
627
+ else:
628
+ present = [k for k, v in r["checklist"].items() if v]
629
+ row.append("Has: " + ", ".join(present) if present else "Missing key files")
630
+ rows.append(row)
631
+
632
+ df = pd.DataFrame(rows, columns=columns)
633
+
634
+ # ── Written briefing ─────────────────────────────────────────────────────
635
+ lines = [
636
+ "## πŸ† Judge's Briefing",
637
+ f"_Analysed {len(results)} submission(s)._\n",
638
+ ]
639
+
640
+ ok_results = [r for r in results if not r["error"]]
641
+ if ok_results:
642
+ top = ok_results[0]
643
+ lines.append(
644
+ f"**Top pick so far:** [{top['title']}]({top['url']})"
645
+ f" β€” **{top['overall']}/10**"
646
+ )
647
+ lines.append(f"> {top['summary']}\n")
648
+
649
+ lines.append("---")
650
+
651
+ for i, r in enumerate(results, start=1):
652
+ lines.append(f"### {i}. {r['title']}")
653
+ if r["url"]:
654
+ lines.append(f"πŸ”— {r['url']}")
655
+ if r["error"]:
656
+ lines.append(f"⚠️ {r['summary']}")
657
+ else:
658
+ lines.append(f"_{r['summary']}_")
659
+ score_bits = " Β· ".join(
660
+ f"{c}: **{s}/10**" for c, s in r["scores"].items()
661
+ )
662
+ lines.append(f"\n**Scores:** {score_bits}")
663
+ lines.append(f"**Overall: {r['overall']}/10** · ❀️ {r['likes']}")
664
+ present = [k for k, v in r["checklist"].items() if v]
665
+ missing = [k for k, v in r["checklist"].items() if not v]
666
+ if present:
667
+ lines.append(f"βœ… {', '.join(present)}")
668
+ if missing:
669
+ lines.append(f"❌ Missing: {', '.join(missing)}")
670
+ lines.append("\n---")
671
+
672
+ return "\n\n".join(lines), df
673
+
674
+
675
+ def run_analysis(urls_text: str, criteria_text: str):
676
+ """Outer wrapper β€” catches any unexpected exception gracefully."""
677
+ try:
678
+ return _run_analysis(urls_text, criteria_text)
679
+ except Exception as exc:
680
+ msg = (
681
+ f"### ⚠️ Something went wrong\n\n"
682
+ f"`{exc}`\n\n"
683
+ f"Double-check the links and try again."
684
+ )
685
+ return msg, pd.DataFrame()
686
+
687
+
688
+ # =============================================================================
689
+ # Gradio UI
690
+ # =============================================================================
691
+ with gr.Blocks(css=CUSTOM_CSS, title="Submission Co-Pilot") as demo:
692
+
693
+ # ── Page 1: Hero ─────────────────────────────────────────────────────────
694
+ with gr.Column(visible=True) as page1:
695
+ gr.HTML(PAGE1_HTML)
696
+ with gr.Row(elem_classes=["nav-row"]):
697
+ btn_to_p2 = gr.Button("Show Me ✨", elem_classes=["cta-btn"])
698
+
699
+ # ── Page 2: Problem ───────────────────────────────────────────────────────
700
+ with gr.Column(visible=False) as page2:
701
+ gr.HTML(PAGE2_HTML)
702
+ with gr.Row(elem_classes=["nav-row"]):
703
+ btn_p2_back = gr.Button("← Back", elem_classes=["ghost-btn"])
704
+ btn_to_p3 = gr.Button("Show Me The Fix β†’", elem_classes=["cta-btn"])
705
+
706
+ # ── Page 3: Analyser ─────────────────────────────────────────────────────
707
+ with gr.Column(visible=False) as page3:
708
+ gr.HTML(PAGE3_HEADER_HTML)
709
+ with gr.Column(elem_classes=["tool-card"]):
710
+ gr.Markdown("#### πŸ”— Step 1 β€” Paste the submissions you are judging")
711
+ urls_box = gr.Textbox(
712
+ label="Hugging Face links β€” one per line",
713
+ placeholder="https://huggingface.co/spaces/username/project-name",
714
+ lines=5,
715
+ )
716
+ gr.Markdown("#### 🎯 Step 2 β€” Tell us what you are scoring for")
717
+ criteria_box = gr.Textbox(
718
+ label="Judging criteria (comma-separated)",
719
+ value="Innovation, Technical Execution, Usability & Design, Real-World Impact, Presentation",
720
+ lines=2,
721
+ )
722
+ analyze_btn = gr.Button("Analyze Submissions πŸ”", elem_classes=["cta-btn"])
723
+ report_md = gr.Markdown(value="")
724
+ leaderboard = gr.Dataframe(label="πŸ“Š Leaderboard")
725
+ gr.HTML("<p class='tool-note'>Automated first pass only β€” the final call is always yours.</p>")
726
+ with gr.Row(elem_classes=["nav-row"]):
727
+ btn_p3_back = gr.Button("← Back", elem_classes=["ghost-btn"])
728
+
729
+ gr.HTML(FOOTER_HTML)
730
+
731
+ # ── Navigation wiring ────────────────────────────────────────────────────
732
+ def show(n):
733
+ return (
734
+ gr.Column(visible=(n == 1)),
735
+ gr.Column(visible=(n == 2)),
736
+ gr.Column(visible=(n == 3)),
737
+ )
738
+
739
+ btn_to_p2.click( fn=lambda: show(2), outputs=[page1, page2, page3])
740
+ btn_to_p3.click( fn=lambda: show(3), outputs=[page1, page2, page3])
741
+ btn_p2_back.click(fn=lambda: show(1), outputs=[page1, page2, page3])
742
+ btn_p3_back.click(fn=lambda: show(2), outputs=[page1, page2, page3])
743
+
744
+ # ── Analyser wiring ──────────────────────────────────────────────────────
745
+ analyze_btn.click(
746
+ fn=run_analysis,
747
+ inputs=[urls_box, criteria_box],
748
+ outputs=[report_md, leaderboard],
749
+ )
750
+
751
+
752
+ if __name__ == "__main__":
753
+ demo.launch()
754
+