YANAbillionarie commited on
Commit
3299fea
Β·
verified Β·
1 Parent(s): 808ece7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +279 -127
app.py CHANGED
@@ -1,8 +1,7 @@
1
 
2
  """
3
- Judges Copilot Β· Gradio x Hugging Face Hackathon 2026
4
- Single page. Paste a hackathon page URL or individual links.
5
- Add your criteria. Get 1st / 2nd / 3rd with full reasons.
6
  """
7
 
8
  import os, re, random
@@ -62,7 +61,7 @@ label span, .label-wrap span {
62
  font-weight: 600 !important;
63
  }
64
 
65
- /* ── analyse button ── */
66
  button.cta {
67
  background: #F5A623 !important;
68
  color: #0b0b18 !important;
@@ -79,6 +78,25 @@ button.cta {
79
  }
80
  button.cta:hover { opacity: .88 !important; transform: translateY(-1px) !important; }
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  /* ── tabs ── */
83
  .tab-nav button {
84
  background: transparent !important;
@@ -131,10 +149,21 @@ button.cta:hover { opacity: .88 !important; transform: translateY(-1px) !importa
131
  .prose hr { border: none !important; border-top: 1px solid #1e1e3a !important; }
132
  .prose, .prose p, .prose li { color: #b8b8e0 !important; }
133
 
 
 
 
 
 
 
 
 
 
 
 
134
  /* ── confetti ── */
135
  @keyframes cffall {
136
- 0% { transform: translateY(-5vh) rotate(0deg); opacity: 1; }
137
- 85% { opacity: 1; }
138
  100% { transform: translateY(105vh) rotate(600deg); opacity: 0; }
139
  }
140
  .cf {
@@ -145,13 +174,22 @@ button.cta:hover { opacity: .88 !important; transform: translateY(-1px) !importa
145
  }
146
  """
147
 
 
 
 
 
 
 
 
 
 
148
  # ═══════════════════════════════════════════════════════════════
149
  # Static HTML
150
  # ═══════════════════════════════════════════════════════════════
151
  HERO_HTML = """
152
  <div style="
153
  background:linear-gradient(180deg,#0b0b18 0%,#10102a 100%);
154
- text-align:center; padding:56px 24px 44px;
155
  border-bottom:1px solid #1a1a34;
156
  font-family:'Inter',sans-serif;
157
  ">
@@ -173,35 +211,49 @@ HERO_HTML = """
173
  </h1>
174
 
175
  <p style="
176
- font-size:.97rem; color:#686898; max-width:450px;
177
- line-height:1.8; margin:0 auto 24px;
178
  ">
179
- Paste the hackathon submissions page β€” or individual links.
180
- Set your criteria. Get a full ranked briefing with
181
- 1st, 2nd, 3rd and a reason for every score.
182
  </p>
183
-
184
- <div style="
185
- background:#111128; border:1px solid #252548;
186
- border-radius:14px 14px 14px 3px; padding:14px 20px;
187
- font-size:.87rem; color:#9090c0; max-width:370px;
188
- margin:0 auto; line-height:1.7; text-align:left;
189
- ">
190
- πŸ‘‹ We read the READMEs. We check the files.
191
- We score every criterion. You just decide.
192
- </div>
193
  </div>
194
  """
195
 
196
- INPUT_HEADER_HTML = """
197
- <div style="font-family:'Inter',sans-serif; padding:30px 0 6px;">
198
  <p style="
199
  font-size:.71rem; color:#F5A623; font-weight:700;
200
- letter-spacing:.1em; text-transform:uppercase; margin-bottom:6px;
201
- ">Step 1 β€” Links</p>
202
- <p style="font-size:.84rem; color:#5858a0; line-height:1.6; margin:0;">
203
- Paste the <strong style="color:#c0c0e8;">hackathon submissions page URL</strong>
204
- (auto-discovers all entries) β€” or paste individual Space links, one per line.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  </p>
206
  </div>
207
  """
@@ -211,22 +263,13 @@ CRITERIA_HEADER_HTML = """
211
  <p style="
212
  font-size:.71rem; color:#F5A623; font-weight:700;
213
  letter-spacing:.1em; text-transform:uppercase; margin-bottom:6px;
214
- ">Step 2 β€” Your Criteria</p>
215
  <p style="font-size:.84rem; color:#5858a0; line-height:1.6; margin:0;">
216
- Add, remove, or rename any criteria β€” these become your scoring rubric.
217
  </p>
218
  </div>
219
  """
220
 
221
- RESULTS_LABEL_HTML = """
222
- <div style="
223
- font-family:'Inter',sans-serif;
224
- font-size:.71rem; color:#F5A623; font-weight:700;
225
- letter-spacing:.1em; text-transform:uppercase;
226
- padding:28px 0 10px;
227
- ">πŸ“Š Results</div>
228
- """
229
-
230
  HANDOFF_HTML = """
231
  <div style="
232
  background:linear-gradient(135deg,#0f0f26,#180f28);
@@ -260,7 +303,7 @@ FOOTER_HTML = """
260
  """
261
 
262
  # ═══════════════════════════════════════════════════════════════
263
- # Scoring signals
264
  # ═══════════════════════════════════════════════════════════════
265
  CRITERIA_SIGNALS = {
266
  "innovation": {
@@ -314,15 +357,22 @@ CRITERIA_ALIASES = {
314
  "completeness":"completeness","overall quality":"completeness",
315
  }
316
 
 
 
 
 
 
 
 
317
  # ═══════════════════════════════════════════════════════════════
318
  # Hub helpers
319
  # ═══════════════════════════════════════════════════════════════
320
  def parse_hf_url(text: str):
321
  text = text.strip().rstrip("/")
322
  for pattern, rtype in [
323
- (r"huggingface\.co/spaces/([\w.\-]+/[\w.\-]+)", "space"),
324
  (r"huggingface\.co/datasets/([\w.\-]+/[\w.\-]+)", "dataset"),
325
- (r"huggingface\.co/([\w.\-]+/[\w.\-]+)", "model"),
326
  ]:
327
  m = re.search(pattern, text)
328
  if m:
@@ -332,11 +382,26 @@ def parse_hf_url(text: str):
332
  return "model", text
333
  return None, None
334
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  def _get_repo_info(repo_type, repo_id):
336
- if repo_type == "space": return api.space_info(repo_id)
337
  if repo_type == "dataset": return api.dataset_info(repo_id)
338
  return api.model_info(repo_id)
339
 
 
340
  def fetch_repo_data(repo_type, repo_id):
341
  data = {"readme":"","files":[],"likes":0,"error":""}
342
  try:
@@ -356,6 +421,7 @@ def fetch_repo_data(repo_type, repo_id):
356
  data["readme"] = ""
357
  return data
358
 
 
359
  def fetch_app_code(repo_type, repo_id, files):
360
  if repo_type != "space": return ""
361
  entry = next((f for f in files if f.lower() in ("app.py","main.py")), None)
@@ -368,37 +434,60 @@ def fetch_app_code(repo_type, repo_id, files):
368
  except Exception:
369
  return ""
370
 
 
371
  def discover_from_hackathon_url(url):
372
  targets = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  tag_match = re.search(r"[?&]tag=([^&\s]+)", url)
374
  if tag_match:
375
  try:
376
  for s in api.list_spaces(filter=tag_match.group(1), limit=100):
377
  sid = getattr(s,"id",None)
378
- if sid: targets.append(sid)
379
- if targets: return targets
 
 
380
  except Exception:
381
  pass
 
 
382
  try:
383
  resp = requests.get(url, timeout=12, headers={"User-Agent":"JudgesCopilot/1.0"})
384
  if resp.ok:
385
  found = re.findall(
386
  r'href=["\'](?:https://huggingface\.co)?/spaces/([\w.\-]+/[\w.\-]+)["\']',
387
  resp.text)
388
- seen = set()
389
  for sid in found:
390
  if sid not in seen:
391
  seen.add(sid); targets.append(sid)
392
  except Exception:
393
  pass
 
394
  return targets
395
 
 
396
  # ═══════════════════════════════════════════════════════════════
397
- # Scoring
398
  # ═══════════════════════════════════════════════════════════════
399
  def _best_signal(criterion_lower):
400
  return CRITERIA_SIGNALS.get(CRITERIA_ALIASES.get(criterion_lower))
401
 
 
402
  def score_criterion(criterion, readme_lower, checklist, repo_type):
403
  score = 5.0
404
  sig = _best_signal(criterion.lower().strip())
@@ -415,6 +504,7 @@ def score_criterion(criterion, readme_lower, checklist, repo_type):
415
  if repo_type == "space" and checklist.get("Uses Gradio"): score += 0.3
416
  return round(max(1.0, min(10.0, score)), 1)
417
 
 
418
  def generate_justification(criterion, score, readme_lower, checklist):
419
  sig = _best_signal(criterion.lower().strip())
420
  parts = []
@@ -430,12 +520,13 @@ def generate_justification(criterion, score, readme_lower, checklist):
430
  if missing: parts.append("❌ missing " + ", ".join(missing))
431
  else:
432
  words = re.findall(r"[a-z]{4,}", criterion.lower())
433
- hits = [w for w in words if w in readme_lower]
434
  parts.append(f"found: _{', '.join(hits[:3])}_" if hits else "no matching keywords")
435
  if len(readme_lower) > 1500: parts.append("well-documented README")
436
  elif len(readme_lower) < 100: parts.append("very sparse README")
437
  return " Β· ".join(parts) if parts else "scored on README signals"
438
 
 
439
  def analyze_submission(raw, criteria_list):
440
  repo_type, repo_id = parse_hf_url(raw)
441
  empty_j = {c:"" for c in criteria_list}
@@ -444,22 +535,22 @@ def analyze_submission(raw, criteria_list):
444
  "scores":{c:0 for c in criteria_list},"justifications":empty_j,
445
  "checklist":{},"summary":"Could not parse as a HF link.","error":True,"likes":0}
446
 
447
- data = fetch_repo_data(repo_type, repo_id)
448
  prefix = {"space":"spaces/","dataset":"datasets/","model":""}[repo_type]
449
- url = f"https://huggingface.co/{prefix}{repo_id}"
450
 
451
  if data["error"]:
452
  return {"input":raw,"title":repo_id,"url":url,"overall":0,
453
  "scores":{c:0 for c in criteria_list},"justifications":empty_j,
454
  "checklist":{},"summary":data["error"],"error":True,"likes":0}
455
 
456
- readme = data["readme"]
457
- files = data["files"]
458
  app_code = fetch_app_code(repo_type, repo_id, files)
459
- body = re.sub(r"^---.*?---","",readme,count=1,flags=re.DOTALL).strip()
460
 
461
  title_m = re.search(r"^#\s+(.+)$", body, re.MULTILINE)
462
- title = title_m.group(1).strip() if title_m else repo_id
463
 
464
  content_lines = []
465
  for ln in body.splitlines():
@@ -471,13 +562,13 @@ def analyze_submission(raw, criteria_list):
471
 
472
  readme_lower = readme.lower()
473
  checklist = {
474
- "Has README": bool(readme.strip()),
475
  "Has requirements.txt": any(f.lower()=="requirements.txt" for f in files),
476
- "Has app entry point": any(f.lower() in ("app.py","main.py") for f in files),
477
- "Has LICENSE": any("license" in f.lower() for f in files),
478
- "Uses Gradio": any(s in app_code for s in
479
  ("import gradio","gr.Blocks","gr.Interface","gr.ChatInterface")),
480
- "Demo media": bool(re.search(r"\.(gif|mp4|png|jpe?g|webm)",readme,re.I))
481
  or "youtube" in readme_lower,
482
  }
483
 
@@ -491,6 +582,7 @@ def analyze_submission(raw, criteria_list):
491
  "scores":scores,"justifications":justifications,"checklist":checklist,
492
  "summary":summary,"error":False,"likes":data["likes"]}
493
 
 
494
  # ═══════════════════════════════════════════════════════════════
495
  # Output builders
496
  # ═══════════════════════════════════════════════════════════════
@@ -498,9 +590,9 @@ def make_confetti():
498
  emojis = ["πŸŽ‰","🎊","✨","πŸ†","⭐","🎈","πŸ₯³","🎯","πŸ’«","🌟","πŸ”₯","πŸ’₯"]
499
  pieces = []
500
  for _ in range(28):
501
- e = random.choice(emojis)
502
- l = random.randint(1,99)
503
- d = round(random.uniform(0,2.2),2)
504
  dur = round(random.uniform(2.5,5),2)
505
  pieces.append(
506
  f'<span class="cf" style="left:{l}%;'
@@ -510,50 +602,32 @@ def make_confetti():
510
 
511
 
512
  def build_podium_html(results):
513
- """Big 1st / 2nd / 3rd winner cards at the top of results."""
514
  ok = [r for r in results if not r["error"]]
515
- if not ok:
516
- return ""
517
-
518
- medals = [("πŸ₯‡","1st","#F5A623","rgba(245,166,35,.15)"),
519
- ("πŸ₯ˆ","2nd","#b0b8d0","rgba(176,184,208,.12)"),
520
- ("πŸ₯‰","3rd","#cd7f32","rgba(205,127,50,.12)")]
521
  cards = []
522
  for i, r in enumerate(ok[:3]):
523
  emoji, place, color, bg = medals[i]
524
  cards.append(f"""
525
- <div style="
526
- background:{bg}; border:1px solid {color}44;
527
- border-radius:14px; padding:20px 16px; text-align:center;
528
- font-family:'Inter',sans-serif; flex:1; min-width:160px;
529
- ">
530
  <div style="font-size:2rem;margin-bottom:6px;">{emoji}</div>
531
  <div style="font-size:.7rem;color:{color};font-weight:700;
532
- letter-spacing:.1em;text-transform:uppercase;margin-bottom:8px;">
533
- {place} Place
534
- </div>
535
  <div style="font-size:.95rem;font-weight:700;color:#fff;
536
- margin-bottom:6px;line-height:1.3;">
537
- {r["title"]}
538
- </div>
539
- <div style="font-size:1.3rem;font-weight:800;color:{color};">
540
- {r["overall"]}/10
541
- </div>
542
  {"<a href='" + r['url'] + "' target='_blank' style='font-size:.75rem;color:#6080c0;'>View β†’</a>" if r["url"] else ""}
543
- </div>
544
- """)
545
-
546
  return f"""
547
  <div style="padding:24px 0 8px;font-family:'Inter',sans-serif;">
548
  <div style="font-size:.71rem;color:#F5A623;font-weight:700;
549
- letter-spacing:.1em;text-transform:uppercase;margin-bottom:14px;">
550
- πŸ† Final Rankings
551
- </div>
552
- <div style="display:flex;gap:12px;flex-wrap:wrap;">
553
- {"".join(cards)}
554
- </div>
555
- </div>
556
- """
557
 
558
 
559
  def build_leaderboard(results, criteria_list):
@@ -587,8 +661,8 @@ def build_briefing(results, criteria_list):
587
  f"_Analysed **{len(results)}** submission(s) Β· **{len(criteria_list)}** criteria_\n",
588
  "---\n"]
589
  for i, r in enumerate(results,1):
590
- medals = {1:"πŸ₯‡",2:"πŸ₯ˆ",3:"πŸ₯‰"}
591
- prefix = medals.get(i, f"**#{i}**")
592
  lines.append(f"### {prefix} {r['title']}")
593
  if r["url"]: lines.append(f"πŸ”— [{r['url']}]({r['url']})")
594
  if r["error"]:
@@ -596,9 +670,9 @@ def build_briefing(results, criteria_list):
596
  else:
597
  lines.append(f"_{r['summary']}_\n")
598
  for c in criteria_list:
599
- s = r["scores"].get(c,0)
600
  just = r["justifications"].get(c,"")
601
- dot = "🟒" if s>=7 else ("🟑" if s>=5 else "πŸ”΄")
602
  lines.append(f"{dot} **{c}: {s}/10** β€” {just}")
603
  lines.append(f"\n**Overall: {r['overall']}/10** Β· ❀️ {r['likes']} likes")
604
  present = [k for k,v in r["checklist"].items() if v]
@@ -610,13 +684,11 @@ def build_briefing(results, criteria_list):
610
 
611
 
612
  # ═══════════════════════════════════════════════════════════════
613
- # Main function
614
  # ═══════════════════════════════════════════════════════════════
615
  def run_analysis(urls_text, criteria_text):
616
- empty = ("", "", pd.DataFrame(), pd.DataFrame(), "")
617
-
618
  if not urls_text or not urls_text.strip():
619
- return ("### πŸ‘€ Paste links and hit Analyse to get started.",
620
  "", pd.DataFrame(), pd.DataFrame(), "")
621
 
622
  criteria_list = [c.strip() for c in re.split(r"[,\n]", criteria_text or "") if c.strip()]
@@ -627,10 +699,13 @@ def run_analysis(urls_text, criteria_text):
627
  for line in urls_text.splitlines():
628
  line = line.strip()
629
  if not line: continue
 
630
  is_listing = (
631
- ("huggingface.co" in line and "?" in line and "/spaces/" not in line)
 
632
  or "?tag=" in line
633
  )
 
634
  if is_listing:
635
  for sid in discover_from_hackathon_url(line):
636
  if sid not in seen:
@@ -640,18 +715,18 @@ def run_analysis(urls_text, criteria_text):
640
  seen.add(line); targets.append(line)
641
 
642
  if not targets:
643
- return ("### πŸ” No valid links found. Paste Space URLs or a hackathon tag page URL.",
644
  "", pd.DataFrame(), pd.DataFrame(), "")
645
 
646
  try:
647
  results = [analyze_submission(t, criteria_list) for t in targets]
648
  results.sort(key=lambda r: r["overall"], reverse=True)
649
 
650
- podium_html = build_podium_html(results)
651
- briefing_md = build_briefing(results, criteria_list)
652
- leaderboard = build_leaderboard(results, criteria_list)
653
- criteria_df = build_criteria_table(results, criteria_list)
654
- confetti = make_confetti() if any(not r["error"] for r in results) else ""
655
 
656
  return briefing_md, podium_html, leaderboard, criteria_df, confetti
657
 
@@ -659,6 +734,12 @@ def run_analysis(urls_text, criteria_text):
659
  return (f"### ⚠️ Error: `{exc}`", "", pd.DataFrame(), pd.DataFrame(), "")
660
 
661
 
 
 
 
 
 
 
662
  # ═══════════════════════════════════════════════════════════════
663
  # UI
664
  # ═══════════════════════════════════════════════════════════════
@@ -666,54 +747,125 @@ with gr.Blocks(theme=gr.themes.Base(), css=CSS, title="Judges Copilot") as demo:
666
 
667
  gr.HTML(HERO_HTML)
668
 
669
- # ── Inputs ───────────────────────────────────────────────────
670
- gr.HTML(INPUT_HEADER_HTML)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
671
  urls_box = gr.Textbox(
672
- label="Hackathon submissions page URL β€” or individual Space links (one per line)",
673
  placeholder=(
674
- "Option A (auto-discovers all): https://huggingface.co/spaces?tag=gradio-hackathon-2026\n"
675
- "Option B (individual): https://huggingface.co/spaces/username/project-name\n"
676
- " another-user/another-space"
677
  ),
678
- lines=5,
 
679
  )
680
 
 
681
  gr.HTML(CRITERIA_HEADER_HTML)
682
  criteria_box = gr.Textbox(
683
- label="Your judging criteria β€” edit, add, or remove any (comma-separated)",
684
  value="Innovation, Technical Execution, Usability & Design, Real-World Impact, Presentation",
685
- lines=3,
 
686
  )
687
 
688
- analyse_btn = gr.Button("⚑ Analyse & Rank Submissions", elem_classes=["cta"])
689
 
690
- # ── Confetti ─────────────────────────────────────────────────
691
  confetti_out = gr.HTML(value="")
692
-
693
- # ── Podium (1st / 2nd / 3rd) ─────────────────────────────────
694
  podium_out = gr.HTML(value="")
695
 
696
- # ── Detailed results ─────────────────────────────────────────
697
- gr.HTML(RESULTS_LABEL_HTML)
 
698
 
699
  with gr.Tabs():
700
  with gr.Tab("πŸ“Š Full Leaderboard"):
701
  leaderboard_out = gr.Dataframe(label="All submissions ranked")
702
  with gr.Tab("🎯 Criteria Breakdown"):
703
- criteria_out = gr.Dataframe(label="Pass / Fail per Criterion (βœ… β‰₯7 Β· 🟑 5-6 Β· ❌ <5)")
704
 
705
  briefing_out = gr.Markdown(value="")
706
 
707
  gr.HTML(HANDOFF_HTML)
708
  gr.HTML(FOOTER_HTML)
709
 
710
- # ── Wiring ───────────────────────────────────────────────────
 
 
 
 
711
  analyse_btn.click(
712
  fn=run_analysis,
713
  inputs=[urls_box, criteria_box],
714
  outputs=[briefing_out, podium_out, leaderboard_out, criteria_out, confetti_out],
715
  )
716
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
717
  if __name__ == "__main__":
718
  demo.launch()
719
 
 
1
 
2
  """
3
+ Judges Copilot Β· Gradio x Hugging Face Hackathon 2026
4
+ One click loads the whole hackathon. No copy-paste needed.
 
5
  """
6
 
7
  import os, re, random
 
61
  font-weight: 600 !important;
62
  }
63
 
64
+ /* ── main CTA button ── */
65
  button.cta {
66
  background: #F5A623 !important;
67
  color: #0b0b18 !important;
 
78
  }
79
  button.cta:hover { opacity: .88 !important; transform: translateY(-1px) !important; }
80
 
81
+ /* ── quick-start chip buttons ── */
82
+ .chip > button {
83
+ background: rgba(245,166,35,.08) !important;
84
+ color: #F5A623 !important;
85
+ border: 1px solid rgba(245,166,35,.28) !important;
86
+ border-radius: 100px !important;
87
+ font-size: .82rem !important;
88
+ font-weight: 700 !important;
89
+ padding: 9px 20px !important;
90
+ white-space: nowrap !important;
91
+ transition: background .18s, border-color .18s, transform .15s !important;
92
+ letter-spacing: .01em !important;
93
+ }
94
+ .chip > button:hover {
95
+ background: rgba(245,166,35,.18) !important;
96
+ border-color: #F5A623 !important;
97
+ transform: translateY(-1px) !important;
98
+ }
99
+
100
  /* ── tabs ── */
101
  .tab-nav button {
102
  background: transparent !important;
 
149
  .prose hr { border: none !important; border-top: 1px solid #1e1e3a !important; }
150
  .prose, .prose p, .prose li { color: #b8b8e0 !important; }
151
 
152
+ /* ── status banner ── */
153
+ .status-banner {
154
+ background: rgba(245,166,35,.07);
155
+ border: 1px solid rgba(245,166,35,.2);
156
+ border-radius: 10px;
157
+ padding: 12px 18px;
158
+ font-size: .85rem;
159
+ color: #c0a060;
160
+ margin: 8px 0 4px;
161
+ }
162
+
163
  /* ── confetti ── */
164
  @keyframes cffall {
165
+ 0% { transform: translateY(-5vh) rotate(0deg); opacity: 1; }
166
+ 85% { opacity: 1; }
167
  100% { transform: translateY(105vh) rotate(600deg); opacity: 0; }
168
  }
169
  .cf {
 
174
  }
175
  """
176
 
177
+ # ═══════════════════════════════════════════════════════════════
178
+ # Preset hackathons β€” add more here any time
179
+ # ═══════════════════════════════════════════════════════════════
180
+ PRESETS = {
181
+ "πŸ† Build Small Hackathon": "https://huggingface.co/build-small-hackathon",
182
+ "🎨 Gradio Hackathon 2026": "https://huggingface.co/spaces?tag=gradio-hackathon-2026",
183
+ "πŸ€– Agents Hackathon": "https://huggingface.co/spaces?tag=agents-hackathon",
184
+ }
185
+
186
  # ═══════════════════════════════════════════════════════════════
187
  # Static HTML
188
  # ═══════════════════════════════════════════════════════════════
189
  HERO_HTML = """
190
  <div style="
191
  background:linear-gradient(180deg,#0b0b18 0%,#10102a 100%);
192
+ text-align:center; padding:56px 24px 40px;
193
  border-bottom:1px solid #1a1a34;
194
  font-family:'Inter',sans-serif;
195
  ">
 
211
  </h1>
212
 
213
  <p style="
214
+ font-size:.97rem; color:#686898; max-width:440px;
215
+ line-height:1.8; margin:0 auto 20px;
216
  ">
217
+ One click loads every submission. We read the READMEs,
218
+ check the files, score every criterion.
219
+ You just decide.
220
  </p>
 
 
 
 
 
 
 
 
 
 
221
  </div>
222
  """
223
 
224
+ QUICKSTART_LABEL_HTML = """
225
+ <div style="font-family:'Inter',sans-serif; padding:28px 0 10px;">
226
  <p style="
227
  font-size:.71rem; color:#F5A623; font-weight:700;
228
+ letter-spacing:.1em; text-transform:uppercase; margin-bottom:10px;
229
+ ">⚑ Quick Start β€” one click, zero copy-paste</p>
230
+ <p style="font-size:.82rem; color:#404068; margin:0 0 12px;">
231
+ Click a hackathon below and we'll load every submission automatically.
232
+ </p>
233
+ </div>
234
+ """
235
+
236
+ OR_DIVIDER_HTML = """
237
+ <div style="
238
+ display:flex; align-items:center; gap:12px;
239
+ font-family:'Inter',sans-serif;
240
+ font-size:.78rem; color:#252548;
241
+ padding: 18px 0 6px;
242
+ ">
243
+ <div style="flex:1; height:1px; background:#1a1a34;"></div>
244
+ <span>or paste your own link</span>
245
+ <div style="flex:1; height:1px; background:#1a1a34;"></div>
246
+ </div>
247
+ """
248
+
249
+ CUSTOM_LABEL_HTML = """
250
+ <div style="font-family:'Inter',sans-serif; padding:4px 0 6px;">
251
+ <p style="
252
+ font-size:.71rem; color:#5858a0; font-weight:700;
253
+ letter-spacing:.1em; text-transform:uppercase; margin-bottom:4px;
254
+ ">Custom Links</p>
255
+ <p style="font-size:.82rem; color:#404068; margin:0;">
256
+ HF org URL Β· individual Space links (one per line) Β· ?tag= filter page
257
  </p>
258
  </div>
259
  """
 
263
  <p style="
264
  font-size:.71rem; color:#F5A623; font-weight:700;
265
  letter-spacing:.1em; text-transform:uppercase; margin-bottom:6px;
266
+ ">Your Criteria</p>
267
  <p style="font-size:.84rem; color:#5858a0; line-height:1.6; margin:0;">
268
+ Edit, add, or remove β€” these become your scoring rubric.
269
  </p>
270
  </div>
271
  """
272
 
 
 
 
 
 
 
 
 
 
273
  HANDOFF_HTML = """
274
  <div style="
275
  background:linear-gradient(135deg,#0f0f26,#180f28);
 
303
  """
304
 
305
  # ═══════════════════════════════════════════════════════════════
306
+ # Scoring
307
  # ═══════════════════════════════════════════════════════════════
308
  CRITERIA_SIGNALS = {
309
  "innovation": {
 
357
  "completeness":"completeness","overall quality":"completeness",
358
  }
359
 
360
+ _HF_SYSTEM_PATHS = {
361
+ "models","datasets","spaces","organizations","pricing",
362
+ "docs","blog","inference-endpoints","enterprise","join",
363
+ "login","register","settings","privacy","terms",
364
+ }
365
+
366
+
367
  # ═══════════════════════════════════════════════════════════════
368
  # Hub helpers
369
  # ═══════════════════════════════════════════════════════════════
370
  def parse_hf_url(text: str):
371
  text = text.strip().rstrip("/")
372
  for pattern, rtype in [
373
+ (r"huggingface\.co/spaces/([\w.\-]+/[\w.\-]+)", "space"),
374
  (r"huggingface\.co/datasets/([\w.\-]+/[\w.\-]+)", "dataset"),
375
+ (r"huggingface\.co/([\w.\-]+/[\w.\-]+)", "model"),
376
  ]:
377
  m = re.search(pattern, text)
378
  if m:
 
382
  return "model", text
383
  return None, None
384
 
385
+
386
+ def _get_org_name(url: str):
387
+ """Return org name if this is a HF org page, else None."""
388
+ if "huggingface.co" not in url:
389
+ return None
390
+ if re.search(r"huggingface\.co/(spaces|datasets|models)/[\w.\-]+/[\w.\-]+", url):
391
+ return None
392
+ m = re.search(r"huggingface\.co/(?:organizations/)?([\w.\-]+)(?:/.*)?$", url)
393
+ if not m:
394
+ return None
395
+ org = m.group(1)
396
+ return None if org in _HF_SYSTEM_PATHS else org
397
+
398
+
399
  def _get_repo_info(repo_type, repo_id):
400
+ if repo_type == "space": return api.space_info(repo_id)
401
  if repo_type == "dataset": return api.dataset_info(repo_id)
402
  return api.model_info(repo_id)
403
 
404
+
405
  def fetch_repo_data(repo_type, repo_id):
406
  data = {"readme":"","files":[],"likes":0,"error":""}
407
  try:
 
421
  data["readme"] = ""
422
  return data
423
 
424
+
425
  def fetch_app_code(repo_type, repo_id, files):
426
  if repo_type != "space": return ""
427
  entry = next((f for f in files if f.lower() in ("app.py","main.py")), None)
 
434
  except Exception:
435
  return ""
436
 
437
+
438
  def discover_from_hackathon_url(url):
439
  targets = []
440
+
441
+ # 1. HF org page
442
+ org = _get_org_name(url)
443
+ if org:
444
+ try:
445
+ for s in api.list_spaces(author=org, limit=200):
446
+ sid = getattr(s, "id", None)
447
+ if sid and sid not in targets:
448
+ targets.append(sid)
449
+ if targets:
450
+ return targets
451
+ except Exception:
452
+ pass
453
+
454
+ # 2. ?tag= filter
455
  tag_match = re.search(r"[?&]tag=([^&\s]+)", url)
456
  if tag_match:
457
  try:
458
  for s in api.list_spaces(filter=tag_match.group(1), limit=100):
459
  sid = getattr(s,"id",None)
460
+ if sid and sid not in targets:
461
+ targets.append(sid)
462
+ if targets:
463
+ return targets
464
  except Exception:
465
  pass
466
+
467
+ # 3. HTML scrape fallback
468
  try:
469
  resp = requests.get(url, timeout=12, headers={"User-Agent":"JudgesCopilot/1.0"})
470
  if resp.ok:
471
  found = re.findall(
472
  r'href=["\'](?:https://huggingface\.co)?/spaces/([\w.\-]+/[\w.\-]+)["\']',
473
  resp.text)
474
+ seen = set(targets)
475
  for sid in found:
476
  if sid not in seen:
477
  seen.add(sid); targets.append(sid)
478
  except Exception:
479
  pass
480
+
481
  return targets
482
 
483
+
484
  # ═══════════════════════════════════════════════════════════════
485
+ # Scoring helpers
486
  # ═══════════════════════════════════════════════════════════════
487
  def _best_signal(criterion_lower):
488
  return CRITERIA_SIGNALS.get(CRITERIA_ALIASES.get(criterion_lower))
489
 
490
+
491
  def score_criterion(criterion, readme_lower, checklist, repo_type):
492
  score = 5.0
493
  sig = _best_signal(criterion.lower().strip())
 
504
  if repo_type == "space" and checklist.get("Uses Gradio"): score += 0.3
505
  return round(max(1.0, min(10.0, score)), 1)
506
 
507
+
508
  def generate_justification(criterion, score, readme_lower, checklist):
509
  sig = _best_signal(criterion.lower().strip())
510
  parts = []
 
520
  if missing: parts.append("❌ missing " + ", ".join(missing))
521
  else:
522
  words = re.findall(r"[a-z]{4,}", criterion.lower())
523
+ hits = [w for w in words if w in readme_lower]
524
  parts.append(f"found: _{', '.join(hits[:3])}_" if hits else "no matching keywords")
525
  if len(readme_lower) > 1500: parts.append("well-documented README")
526
  elif len(readme_lower) < 100: parts.append("very sparse README")
527
  return " Β· ".join(parts) if parts else "scored on README signals"
528
 
529
+
530
  def analyze_submission(raw, criteria_list):
531
  repo_type, repo_id = parse_hf_url(raw)
532
  empty_j = {c:"" for c in criteria_list}
 
535
  "scores":{c:0 for c in criteria_list},"justifications":empty_j,
536
  "checklist":{},"summary":"Could not parse as a HF link.","error":True,"likes":0}
537
 
538
+ data = fetch_repo_data(repo_type, repo_id)
539
  prefix = {"space":"spaces/","dataset":"datasets/","model":""}[repo_type]
540
+ url = f"https://huggingface.co/{prefix}{repo_id}"
541
 
542
  if data["error"]:
543
  return {"input":raw,"title":repo_id,"url":url,"overall":0,
544
  "scores":{c:0 for c in criteria_list},"justifications":empty_j,
545
  "checklist":{},"summary":data["error"],"error":True,"likes":0}
546
 
547
+ readme = data["readme"]
548
+ files = data["files"]
549
  app_code = fetch_app_code(repo_type, repo_id, files)
550
+ body = re.sub(r"^---.*?---","",readme,count=1,flags=re.DOTALL).strip()
551
 
552
  title_m = re.search(r"^#\s+(.+)$", body, re.MULTILINE)
553
+ title = title_m.group(1).strip() if title_m else repo_id
554
 
555
  content_lines = []
556
  for ln in body.splitlines():
 
562
 
563
  readme_lower = readme.lower()
564
  checklist = {
565
+ "Has README": bool(readme.strip()),
566
  "Has requirements.txt": any(f.lower()=="requirements.txt" for f in files),
567
+ "Has app entry point": any(f.lower() in ("app.py","main.py") for f in files),
568
+ "Has LICENSE": any("license" in f.lower() for f in files),
569
+ "Uses Gradio": any(s in app_code for s in
570
  ("import gradio","gr.Blocks","gr.Interface","gr.ChatInterface")),
571
+ "Demo media": bool(re.search(r"\.(gif|mp4|png|jpe?g|webm)",readme,re.I))
572
  or "youtube" in readme_lower,
573
  }
574
 
 
582
  "scores":scores,"justifications":justifications,"checklist":checklist,
583
  "summary":summary,"error":False,"likes":data["likes"]}
584
 
585
+
586
  # ═══════════════════════════════════════════════════════════════
587
  # Output builders
588
  # ═══════════════════════════════════════════════════════════════
 
590
  emojis = ["πŸŽ‰","🎊","✨","πŸ†","⭐","🎈","πŸ₯³","🎯","πŸ’«","🌟","πŸ”₯","πŸ’₯"]
591
  pieces = []
592
  for _ in range(28):
593
+ e = random.choice(emojis)
594
+ l = random.randint(1,99)
595
+ d = round(random.uniform(0,2.2),2)
596
  dur = round(random.uniform(2.5,5),2)
597
  pieces.append(
598
  f'<span class="cf" style="left:{l}%;'
 
602
 
603
 
604
  def build_podium_html(results):
 
605
  ok = [r for r in results if not r["error"]]
606
+ if not ok: return ""
607
+ medals = [("πŸ₯‡","1st","#F5A623","rgba(245,166,35,.15)"),
608
+ ("πŸ₯ˆ","2nd","#b0b8d0","rgba(176,184,208,.12)"),
609
+ ("πŸ₯‰","3rd","#cd7f32","rgba(205,127,50,.12)")]
 
 
610
  cards = []
611
  for i, r in enumerate(ok[:3]):
612
  emoji, place, color, bg = medals[i]
613
  cards.append(f"""
614
+ <div style="background:{bg};border:1px solid {color}44;border-radius:14px;
615
+ padding:20px 16px;text-align:center;font-family:'Inter',sans-serif;
616
+ flex:1;min-width:160px;">
 
 
617
  <div style="font-size:2rem;margin-bottom:6px;">{emoji}</div>
618
  <div style="font-size:.7rem;color:{color};font-weight:700;
619
+ letter-spacing:.1em;text-transform:uppercase;margin-bottom:8px;">{place} Place</div>
 
 
620
  <div style="font-size:.95rem;font-weight:700;color:#fff;
621
+ margin-bottom:6px;line-height:1.3;">{r["title"]}</div>
622
+ <div style="font-size:1.3rem;font-weight:800;color:{color};">{r["overall"]}/10</div>
 
 
 
 
623
  {"<a href='" + r['url'] + "' target='_blank' style='font-size:.75rem;color:#6080c0;'>View β†’</a>" if r["url"] else ""}
624
+ </div>""")
 
 
625
  return f"""
626
  <div style="padding:24px 0 8px;font-family:'Inter',sans-serif;">
627
  <div style="font-size:.71rem;color:#F5A623;font-weight:700;
628
+ letter-spacing:.1em;text-transform:uppercase;margin-bottom:14px;">πŸ† Final Rankings</div>
629
+ <div style="display:flex;gap:12px;flex-wrap:wrap;">{"".join(cards)}</div>
630
+ </div>"""
 
 
 
 
 
631
 
632
 
633
  def build_leaderboard(results, criteria_list):
 
661
  f"_Analysed **{len(results)}** submission(s) Β· **{len(criteria_list)}** criteria_\n",
662
  "---\n"]
663
  for i, r in enumerate(results,1):
664
+ medals_map = {1:"πŸ₯‡",2:"πŸ₯ˆ",3:"πŸ₯‰"}
665
+ prefix = medals_map.get(i, f"**#{i}**")
666
  lines.append(f"### {prefix} {r['title']}")
667
  if r["url"]: lines.append(f"πŸ”— [{r['url']}]({r['url']})")
668
  if r["error"]:
 
670
  else:
671
  lines.append(f"_{r['summary']}_\n")
672
  for c in criteria_list:
673
+ s = r["scores"].get(c,0)
674
  just = r["justifications"].get(c,"")
675
+ dot = "🟒" if s>=7 else ("🟑" if s>=5 else "πŸ”΄")
676
  lines.append(f"{dot} **{c}: {s}/10** β€” {just}")
677
  lines.append(f"\n**Overall: {r['overall']}/10** Β· ❀️ {r['likes']} likes")
678
  present = [k for k,v in r["checklist"].items() if v]
 
684
 
685
 
686
  # ═══════════════════════════════════════════════════════════════
687
+ # Main analysis function
688
  # ═══════════════════════════════════════════════════════════════
689
  def run_analysis(urls_text, criteria_text):
 
 
690
  if not urls_text or not urls_text.strip():
691
+ return ("### πŸ‘€ Click a hackathon above or paste a link and hit Analyse.",
692
  "", pd.DataFrame(), pd.DataFrame(), "")
693
 
694
  criteria_list = [c.strip() for c in re.split(r"[,\n]", criteria_text or "") if c.strip()]
 
699
  for line in urls_text.splitlines():
700
  line = line.strip()
701
  if not line: continue
702
+
703
  is_listing = (
704
+ _get_org_name(line) is not None
705
+ or ("huggingface.co" in line and "?" in line and "/spaces/" not in line)
706
  or "?tag=" in line
707
  )
708
+
709
  if is_listing:
710
  for sid in discover_from_hackathon_url(line):
711
  if sid not in seen:
 
715
  seen.add(line); targets.append(line)
716
 
717
  if not targets:
718
+ return ("### πŸ” No valid links found. Try clicking a Quick Start button above.",
719
  "", pd.DataFrame(), pd.DataFrame(), "")
720
 
721
  try:
722
  results = [analyze_submission(t, criteria_list) for t in targets]
723
  results.sort(key=lambda r: r["overall"], reverse=True)
724
 
725
+ podium_html = build_podium_html(results)
726
+ briefing_md = build_briefing(results, criteria_list)
727
+ leaderboard = build_leaderboard(results, criteria_list)
728
+ criteria_df = build_criteria_table(results, criteria_list)
729
+ confetti = make_confetti() if any(not r["error"] for r in results) else ""
730
 
731
  return briefing_md, podium_html, leaderboard, criteria_df, confetti
732
 
 
734
  return (f"### ⚠️ Error: `{exc}`", "", pd.DataFrame(), pd.DataFrame(), "")
735
 
736
 
737
+ def fill_and_run(preset_url, criteria_text):
738
+ """Fill the URL box AND immediately run analysis β€” one click does both."""
739
+ briefing, podium, lb, crit_df, confetti = run_analysis(preset_url, criteria_text)
740
+ return preset_url, briefing, podium, lb, crit_df, confetti
741
+
742
+
743
  # ═══════════════════════════════════════════════════════════════
744
  # UI
745
  # ═══════════════════════════════════════════════════════════════
 
747
 
748
  gr.HTML(HERO_HTML)
749
 
750
+ # ── Quick-start chips ────────────────────────────────────────────────────
751
+ gr.HTML(QUICKSTART_LABEL_HTML)
752
+
753
+ with gr.Row():
754
+ chip_bs = gr.Button("πŸ† Build Small Hackathon", size="sm", elem_classes=["chip"])
755
+ chip_gh = gr.Button("🎨 Gradio Hackathon 2026", size="sm", elem_classes=["chip"])
756
+ chip_ag = gr.Button("πŸ€– Agents Hackathon", size="sm", elem_classes=["chip"])
757
+
758
+ # ── Status line (shows which hackathon was loaded) ────────────────────
759
+ status_md = gr.Markdown(value="", visible=False)
760
+
761
+ # ── Divider ──────────────────────────────────────────────────────────────
762
+ gr.HTML(OR_DIVIDER_HTML)
763
+
764
+ # ── Custom URL input ──────────────────────────────────────────────────────
765
+ gr.HTML(CUSTOM_LABEL_HTML)
766
  urls_box = gr.Textbox(
767
+ label="",
768
  placeholder=(
769
+ "https://huggingface.co/your-org\n"
770
+ "https://huggingface.co/spaces/username/project\n"
771
+ "https://huggingface.co/spaces?tag=your-tag"
772
  ),
773
+ lines=3,
774
+ show_label=False,
775
  )
776
 
777
+ # ── Criteria ──────────────────────────────────────────────────────────────
778
  gr.HTML(CRITERIA_HEADER_HTML)
779
  criteria_box = gr.Textbox(
780
+ label="",
781
  value="Innovation, Technical Execution, Usability & Design, Real-World Impact, Presentation",
782
+ lines=2,
783
+ show_label=False,
784
  )
785
 
786
+ analyse_btn = gr.Button("⚑ Analyse & Rank", elem_classes=["cta"])
787
 
788
+ # ── Outputs ───────────────────────────────────────────────────────────────
789
  confetti_out = gr.HTML(value="")
 
 
790
  podium_out = gr.HTML(value="")
791
 
792
+ gr.HTML("""<div style="font-family:'Inter',sans-serif;font-size:.71rem;color:#F5A623;
793
+ font-weight:700;letter-spacing:.1em;text-transform:uppercase;padding:28px 0 10px;">
794
+ πŸ“Š Results</div>""")
795
 
796
  with gr.Tabs():
797
  with gr.Tab("πŸ“Š Full Leaderboard"):
798
  leaderboard_out = gr.Dataframe(label="All submissions ranked")
799
  with gr.Tab("🎯 Criteria Breakdown"):
800
+ criteria_out = gr.Dataframe(label="βœ… β‰₯7 Β· 🟑 5-6 Β· ❌ <5")
801
 
802
  briefing_out = gr.Markdown(value="")
803
 
804
  gr.HTML(HANDOFF_HTML)
805
  gr.HTML(FOOTER_HTML)
806
 
807
+ # ═══════════════════════════════════════════════════════
808
+ # Event wiring
809
+ # ═══════════════════════════════════════════════════════
810
+
811
+ # Manual analyse button
812
  analyse_btn.click(
813
  fn=run_analysis,
814
  inputs=[urls_box, criteria_box],
815
  outputs=[briefing_out, podium_out, leaderboard_out, criteria_out, confetti_out],
816
  )
817
 
818
+ # ── Chip: Build Small Hackathon ──────────────────────────────────────
819
+ chip_bs.click(
820
+ fn=lambda ct: (
821
+ PRESETS["πŸ† Build Small Hackathon"],
822
+ gr.update(value=f"⏳ Loading all submissions from **Build Small Hackathon**…", visible=True),
823
+ ),
824
+ inputs=[criteria_box],
825
+ outputs=[urls_box, status_md],
826
+ ).then(
827
+ fn=fill_and_run,
828
+ inputs=[urls_box, criteria_box],
829
+ outputs=[urls_box, briefing_out, podium_out, leaderboard_out, criteria_out, confetti_out],
830
+ ).then(
831
+ fn=lambda: gr.update(value="βœ… Done β€” scroll down for results", visible=True),
832
+ outputs=[status_md],
833
+ )
834
+
835
+ # ── Chip: Gradio Hackathon ───────────────────────────────────────────
836
+ chip_gh.click(
837
+ fn=lambda ct: (
838
+ PRESETS["🎨 Gradio Hackathon 2026"],
839
+ gr.update(value=f"⏳ Loading all submissions from **Gradio Hackathon 2026**…", visible=True),
840
+ ),
841
+ inputs=[criteria_box],
842
+ outputs=[urls_box, status_md],
843
+ ).then(
844
+ fn=fill_and_run,
845
+ inputs=[urls_box, criteria_box],
846
+ outputs=[urls_box, briefing_out, podium_out, leaderboard_out, criteria_out, confetti_out],
847
+ ).then(
848
+ fn=lambda: gr.update(value="βœ… Done β€” scroll down for results", visible=True),
849
+ outputs=[status_md],
850
+ )
851
+
852
+ # ── Chip: Agents Hackathon ───────────────────────────────────────────
853
+ chip_ag.click(
854
+ fn=lambda ct: (
855
+ PRESETS["πŸ€– Agents Hackathon"],
856
+ gr.update(value=f"⏳ Loading all submissions from **Agents Hackathon**…", visible=True),
857
+ ),
858
+ inputs=[criteria_box],
859
+ outputs=[urls_box, status_md],
860
+ ).then(
861
+ fn=fill_and_run,
862
+ inputs=[urls_box, criteria_box],
863
+ outputs=[urls_box, briefing_out, podium_out, leaderboard_out, criteria_out, confetti_out],
864
+ ).then(
865
+ fn=lambda: gr.update(value="βœ… Done β€” scroll down for results", visible=True),
866
+ outputs=[status_md],
867
+ )
868
+
869
  if __name__ == "__main__":
870
  demo.launch()
871