fffiloni commited on
Commit
217de2a
·
verified ·
1 Parent(s): d5ebf36

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +630 -1251
app.py CHANGED
@@ -363,57 +363,165 @@ def empty_activity_bucket(title: str):
363
  }
364
 
365
 
366
- # ============================================================
367
- # Community activity fetch
368
- # ============================================================
369
- def empty_community_state(selected_year: int):
370
- return {
 
371
  "username": "",
372
  "selected_year": int(selected_year),
373
  "fetched": False,
374
- "pr_opened": empty_activity_bucket("Pull Requests"),
375
- "pr_activity": empty_activity_bucket("PR Activity"),
376
- "thread_opened": empty_activity_bucket("Community Threads"),
377
- "discussion_activity": empty_activity_bucket("Community Activity"),
378
- "logs": "Community activity not fetched yet.",
379
  }
380
 
 
 
381
 
382
- def fetch_user_discussion_activity(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  username: str,
384
  selected_year: int,
385
- mode: str,
 
 
 
 
 
386
  ):
387
  username = (username or "").strip()
388
  selected_year = int(selected_year)
389
 
390
- title_map = {
391
- "pr_opened": "Pull Requests",
392
- "pr_activity": "PR Activity",
393
- "thread_opened": "Community Threads",
394
- "discussion_activity": "Community Activity",
395
- }
396
- title = title_map.get(mode, "Community")
397
 
398
  if not username:
399
- return {
400
- "title": title,
401
- "fetched": True,
402
- "dates": [],
403
- "count": 0,
404
- "logs": "No username provided.",
405
- }
406
 
407
- dates = []
408
- seen_ids = set()
 
 
409
  skip = 0
410
  limit = 20
411
- cursor = None
412
  pages_fetched = 0
413
 
414
  MAX_PAGES = 40
415
- MAX_DURATION = 25
416
- OPEN_EVENT_EPSILON_SECONDS = 90
417
  start = time.time()
418
 
419
  logs = []
@@ -427,94 +535,6 @@ def fetch_user_discussion_activity(
427
  }
428
  )
429
 
430
- def normalize_payload(payload):
431
- if isinstance(payload, list):
432
- return payload, None
433
-
434
- if isinstance(payload, dict):
435
- events = payload.get("recentActivity")
436
- if isinstance(events, list):
437
- return events, payload.get("cursor")
438
-
439
- for key in ["items", "events", "activity"]:
440
- value = payload.get(key)
441
- if isinstance(value, list):
442
- return value, payload.get("cursor")
443
-
444
- return [], None
445
-
446
- def extract_author_name(discussion: dict) -> str | None:
447
- author = discussion.get("author")
448
- if isinstance(author, str):
449
- return author
450
- if isinstance(author, dict):
451
- for key in ["name", "username", "login"]:
452
- value = author.get(key)
453
- if isinstance(value, str):
454
- return value
455
- return None
456
-
457
- def parse_ts(raw_value):
458
- if not raw_value:
459
- return None
460
- try:
461
- return pd.to_datetime(raw_value, utc=True)
462
- except Exception:
463
- return None
464
-
465
- def event_matches_mode(event: dict, discussion: dict) -> tuple[bool, str | None]:
466
- is_pr = bool(discussion.get("isPullRequest", False))
467
- author_name = extract_author_name(discussion)
468
- repo_id = event.get("repoId", "unknown-repo")
469
- discussion_num = discussion.get("num", "unknown-num")
470
- event_id = event.get("eventId", "unknown-event")
471
-
472
- if mode == "pr_opened":
473
- if not is_pr:
474
- return False, None
475
- if not author_name or author_name.lower() != username.lower():
476
- return False, None
477
- return True, f"{repo_id}::pr::{discussion_num}"
478
-
479
- if mode == "pr_activity":
480
- if not is_pr:
481
- return False, None
482
- return True, f"event::{event_id}"
483
-
484
- if mode == "thread_opened":
485
- if is_pr:
486
- return False, None
487
- if not author_name or author_name.lower() != username.lower():
488
- return False, None
489
- return True, f"{repo_id}::thread::{discussion_num}"
490
-
491
- if mode == "discussion_activity":
492
- if is_pr:
493
- return False, None
494
- return True, f"event::{event_id}"
495
-
496
- return False, None
497
-
498
- def extract_date_for_mode(event: dict, discussion: dict):
499
- event_ts = parse_ts(event.get("time"))
500
- created_ts = parse_ts(discussion.get("createdAt"))
501
-
502
- if mode in {"pr_opened", "thread_opened"}:
503
- if created_ts is None:
504
- return None
505
- return created_ts.tz_localize(None).date()
506
-
507
- if mode in {"pr_activity", "discussion_activity"}:
508
- if event_ts is None:
509
- return None
510
- if created_ts is not None:
511
- delta_seconds = abs((event_ts - created_ts).total_seconds())
512
- if delta_seconds <= OPEN_EVENT_EPSILON_SECONDS:
513
- return None
514
- return event_ts.tz_localize(None).date()
515
-
516
- return None
517
-
518
  try:
519
  while True:
520
  if time.time() - start > MAX_DURATION:
@@ -529,7 +549,7 @@ def fetch_user_discussion_activity(
529
 
530
  params = {
531
  "limit": limit,
532
- "activityType": "discussion",
533
  "feedType": "user",
534
  "entity": username,
535
  }
@@ -548,7 +568,8 @@ def fetch_user_discussion_activity(
548
  payload = response.json()
549
  pages_fetched += 1
550
 
551
- events, next_cursor = normalize_payload(payload)
 
552
 
553
  if not events:
554
  logs.append("No more events.")
@@ -560,28 +581,35 @@ def fetch_user_discussion_activity(
560
  if not isinstance(event, dict):
561
  continue
562
 
563
- event_ts = parse_ts(event.get("time"))
564
- if event_ts is not None and event_ts.year < selected_year:
 
 
 
 
 
 
 
 
565
  stop_early = True
566
  break
567
 
568
- discussion = event.get("discussionData") or {}
569
- if not isinstance(discussion, dict):
570
  continue
571
 
572
- keep, unique_id = event_matches_mode(event, discussion)
573
- if not keep or not unique_id:
 
574
  continue
575
 
576
- dt = extract_date_for_mode(event, discussion)
577
- if dt is None or dt.year != selected_year:
578
  continue
579
 
580
- if unique_id in seen_ids:
581
  continue
582
 
583
- seen_ids.add(unique_id)
584
- dates.append(dt)
585
 
586
  if stop_early:
587
  logs.append("Stopped early after reaching feed events older than selected year.")
@@ -592,251 +620,11 @@ def fetch_user_discussion_activity(
592
  else:
593
  skip += limit
594
 
595
- logs.append(f"Fetched {pages_fetched} page(s) from recent-activity.")
596
- logs.append(f"Found {len(dates)} unique {mode} event(s) in {selected_year}.")
597
-
598
- return {
599
- "title": title,
600
  "fetched": True,
601
- "dates": dates,
602
- "count": len(dates),
603
- "logs": "\n".join(logs),
604
- }
605
-
606
- except Exception as e:
607
- return {
608
- "title": title,
609
- "fetched": True,
610
- "dates": [],
611
- "count": 0,
612
- "logs": f"{title} fetch failed: {str(e)}",
613
- }
614
-
615
-
616
- def fetch_all_community_activity(username: str, selected_year: int):
617
- username = (username or "").strip()
618
- selected_year = int(selected_year)
619
-
620
- cache_key = (username.lower(), selected_year, "community")
621
- cached = get_cache(cache_key)
622
- if cached:
623
- return cached
624
-
625
- if not username:
626
- state = empty_community_state(selected_year)
627
- state["fetched"] = True
628
- state["logs"] = "No username provided."
629
- set_cache(cache_key, state)
630
- return state
631
-
632
- pr_opened = fetch_user_discussion_activity(username, selected_year, "pr_opened")
633
- pr_activity = fetch_user_discussion_activity(username, selected_year, "pr_activity")
634
- thread_opened = fetch_user_discussion_activity(username, selected_year, "thread_opened")
635
- discussion_activity = fetch_user_discussion_activity(username, selected_year, "discussion_activity")
636
-
637
- combined_logs = "\n\n".join(
638
- [
639
- "[Pull Requests]\n" + pr_opened["logs"],
640
- "[PR Activity]\n" + pr_activity["logs"],
641
- "[Community Threads]\n" + thread_opened["logs"],
642
- "[Community Activity]\n" + discussion_activity["logs"],
643
- ]
644
- )
645
-
646
- result = {
647
- "username": username,
648
- "selected_year": selected_year,
649
- "fetched": True,
650
- "pr_opened": pr_opened,
651
- "pr_activity": pr_activity,
652
- "thread_opened": thread_opened,
653
- "discussion_activity": discussion_activity,
654
- "logs": combined_logs,
655
- }
656
-
657
- set_cache(cache_key, result)
658
- return result
659
-
660
-
661
- # ============================================================
662
- # Generic bucketed recent-activity fetch helpers
663
- # ============================================================
664
- def empty_bucketed_activity_state(
665
- selected_year: int,
666
- logs_text: str,
667
- bucket_titles: dict[str, str],
668
- ):
669
- state = {
670
- "username": "",
671
- "selected_year": int(selected_year),
672
- "fetched": False,
673
- "logs": logs_text,
674
- }
675
-
676
- for key, title in bucket_titles.items():
677
- state[key] = empty_activity_bucket(title)
678
-
679
- return state
680
-
681
-
682
- def build_fetched_bucket(title: str, dates: list, selected_year: int, label: str):
683
- return {
684
- "title": title,
685
- "fetched": True,
686
- "dates": dates,
687
- "count": len(dates),
688
- "logs": f"Found {len(dates)} {label} in {selected_year}.",
689
- }
690
-
691
-
692
- def build_failed_bucket(title: str):
693
- return {
694
- "title": title,
695
- "fetched": True,
696
- "dates": [],
697
- "count": 0,
698
- "logs": "",
699
- }
700
-
701
-
702
- def fetch_bucketed_recent_activity(
703
- username: str,
704
- selected_year: int,
705
- cache_scope: str,
706
- activity_type: str,
707
- bucket_titles: dict[str, str],
708
- bucket_log_labels: dict[str, str],
709
- empty_logs_text: str,
710
- classifier,
711
- ):
712
- username = (username or "").strip()
713
- selected_year = int(selected_year)
714
-
715
- cache_key = (username.lower(), selected_year, cache_scope)
716
- cached = get_cache(cache_key)
717
- if cached:
718
- return cached
719
-
720
- if not username:
721
- state = empty_bucketed_activity_state(selected_year, empty_logs_text, bucket_titles)
722
- state["fetched"] = True
723
- state["logs"] = "No username provided."
724
- set_cache(cache_key, state)
725
- return state
726
-
727
- bucket_dates = {key: [] for key in bucket_titles}
728
- bucket_seen_ids = {key: set() for key in bucket_titles}
729
-
730
- cursor = None
731
- skip = 0
732
- limit = 20
733
- pages_fetched = 0
734
-
735
- MAX_PAGES = 40
736
- MAX_DURATION = 20
737
- start = time.time()
738
-
739
- logs = []
740
-
741
- session = requests.Session()
742
- session.headers.update(
743
- {
744
- "User-Agent": "hf-contributions-app/1.0",
745
- "Accept": "application/json",
746
- "Cache-Control": "no-cache",
747
- }
748
- )
749
-
750
- try:
751
- while True:
752
- if time.time() - start > MAX_DURATION:
753
- logs.append("Stopped due to timeout.")
754
- break
755
-
756
- if pages_fetched >= MAX_PAGES:
757
- logs.append("Stopped due to max pages.")
758
- break
759
-
760
- recent_activity_rate_limiter.wait()
761
-
762
- params = {
763
- "limit": limit,
764
- "activityType": activity_type,
765
- "feedType": "user",
766
- "entity": username,
767
- }
768
- if cursor:
769
- params["cursor"] = cursor
770
- else:
771
- params["skip"] = skip
772
-
773
- response = session.get(
774
- "https://huggingface.co/api/recent-activity",
775
- params=params,
776
- timeout=20,
777
- )
778
- response.raise_for_status()
779
-
780
- payload = response.json()
781
- pages_fetched += 1
782
-
783
- events = payload.get("recentActivity", [])
784
- next_cursor = payload.get("cursor")
785
-
786
- if not events:
787
- logs.append("No more events.")
788
- break
789
-
790
- stop_early = False
791
-
792
- for event in events:
793
- if not isinstance(event, dict):
794
- continue
795
-
796
- raw_time = event.get("time")
797
- if not raw_time:
798
- continue
799
-
800
- try:
801
- event_ts = pd.to_datetime(raw_time, utc=True)
802
- except Exception:
803
- continue
804
-
805
- if event_ts.year < selected_year:
806
- stop_early = True
807
- break
808
-
809
- if event_ts.year != selected_year:
810
- continue
811
-
812
- bucket_key, unique_id = classifier(event)
813
-
814
- if not bucket_key or not unique_id:
815
- continue
816
-
817
- if bucket_key not in bucket_dates:
818
- continue
819
-
820
- if unique_id in bucket_seen_ids[bucket_key]:
821
- continue
822
-
823
- bucket_seen_ids[bucket_key].add(unique_id)
824
- bucket_dates[bucket_key].append(event_ts.tz_localize(None).date())
825
-
826
- if stop_early:
827
- logs.append("Stopped early after reaching feed events older than selected year.")
828
- break
829
-
830
- if next_cursor:
831
- cursor = next_cursor
832
- else:
833
- skip += limit
834
-
835
- result = {
836
- "username": username,
837
- "selected_year": selected_year,
838
- "fetched": True,
839
- "logs": "",
840
  }
841
 
842
  for key, title in bucket_titles.items():
@@ -936,173 +724,339 @@ def classify_paper_event(event: dict):
936
  return bucket_key, unique_id
937
 
938
 
939
- # ============================================================
940
- # Upvotes activity fetch
941
- # ============================================================
942
- def empty_upvotes_state(selected_year: int):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
943
  return empty_bucketed_activity_state(
944
  selected_year=selected_year,
945
- logs_text="Upvotes activity not fetched yet.",
946
- bucket_titles={
947
- "papers": "Paper Upvotes",
948
- "blog": "Blog Upvotes",
949
- "changelog": "Changelog Upvotes",
950
- },
951
  )
952
 
953
 
954
- def fetch_all_upvotes_activity(username: str, selected_year: int):
 
 
955
  return fetch_bucketed_recent_activity(
956
  username=username,
957
  selected_year=selected_year,
958
- cache_scope="upvotes",
959
- activity_type="upvote",
960
- bucket_titles={
961
- "papers": "Paper Upvotes",
962
- "blog": "Blog Upvotes",
963
- "changelog": "Changelog Upvotes",
964
- },
965
- bucket_log_labels={
966
- "papers": "paper upvote(s)",
967
- "blog": "blog upvote(s)",
968
- "changelog": "changelog upvote(s)",
969
- },
970
- empty_logs_text="Upvotes activity not fetched yet.",
971
- classifier=classify_upvote_event,
972
  )
973
 
974
 
975
  # ============================================================
976
- # Likes activity fetch
977
  # ============================================================
978
- def empty_likes_state(selected_year: int):
979
- return empty_bucketed_activity_state(
980
- selected_year=selected_year,
981
- logs_text="Likes activity not fetched yet.",
982
- bucket_titles={
983
- "models": "Model Likes",
984
- "datasets": "Dataset Likes",
985
- "spaces": "Space Likes",
986
- },
987
- )
 
988
 
989
 
990
- def fetch_all_likes_activity(username: str, selected_year: int):
991
- return fetch_bucketed_recent_activity(
992
- username=username,
993
- selected_year=selected_year,
994
- cache_scope="likes",
995
- activity_type="like",
996
- bucket_titles={
997
- "models": "Model Likes",
998
- "datasets": "Dataset Likes",
999
- "spaces": "Space Likes",
1000
- },
1001
- bucket_log_labels={
1002
- "models": "model like(s)",
1003
- "datasets": "dataset like(s)",
1004
- "spaces": "space like(s)",
1005
- },
1006
- empty_logs_text="Likes activity not fetched yet.",
1007
- classifier=classify_like_event,
1008
- )
1009
 
 
 
 
 
 
 
 
1010
 
1011
- # ============================================================
1012
- # Papers activity fetch
1013
- # ============================================================
1014
- def empty_papers_state(selected_year: int):
1015
- return empty_bucketed_activity_state(
1016
- selected_year=selected_year,
1017
- logs_text="Papers activity not fetched yet.",
1018
- bucket_titles={
1019
- "daily": "Daily Papers",
1020
- "authored": "Authored Papers",
1021
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1022
  )
1023
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1024
 
1025
- def fetch_all_papers_activity(username: str, selected_year: int):
1026
- return fetch_bucketed_recent_activity(
1027
- username=username,
1028
- selected_year=selected_year,
1029
- cache_scope="papers",
1030
- activity_type="update-paper",
1031
- bucket_titles={
1032
- "daily": "Daily Papers",
1033
- "authored": "Authored Papers",
1034
- },
1035
- bucket_log_labels={
1036
- "daily": "daily paper submission(s)",
1037
- "authored": "authored paper(s)",
1038
- },
1039
- empty_logs_text="Papers activity not fetched yet.",
1040
- classifier=classify_paper_event,
1041
- )
1042
 
 
 
 
 
 
 
 
1043
 
1044
- # ============================================================
1045
- # Contribution grid helpers
1046
- # ============================================================
1047
- PALETTES = {
1048
- "green": ["#0e4429", "#006d32", "#26a641", "#39d353"],
1049
- "blue": ["#0a3069", "#0969da", "#54aeff", "#79c0ff"],
1050
- "pink": ["#6e1f4d", "#bf3989", "#db61a2", "#f778ba"],
1051
- "orange": ["#5f370e", "#a14d00", "#d97706", "#f59e0b"],
1052
- "purple": ["#4c1d95", "#6d28d9", "#8b5cf6", "#c4b5fd"],
1053
- "teal": ["#0f766e", "#0d9488", "#14b8a6", "#5eead4"],
1054
- }
1055
-
1056
- TOOLTIP_SAFE_COLUMNS = 2
1057
-
1058
-
1059
- def level_from_count(count: int) -> int:
1060
- if count <= 0:
1061
- return 0
1062
- if count == 1:
1063
- return 1
1064
- if count <= 3:
1065
- return 2
1066
- if count <= 7:
1067
- return 3
1068
- return 4
1069
-
1070
-
1071
- def level_color(palette_name: str, level: int) -> str:
1072
- if level <= 0:
1073
- return "var(--gh-empty-cell)"
1074
- palette = PALETTES[palette_name]
1075
- return palette[min(level - 1, len(palette) - 1)]
1076
-
1077
-
1078
- def build_daily_count_map(df: pd.DataFrame):
1079
- if df.empty:
1080
- return {}
1081
-
1082
- counts = (
1083
- df.copy()
1084
- .assign(date=pd.to_datetime(df["date"]).dt.date, count=1)
1085
- .groupby("date", as_index=False)["count"]
1086
- .sum()
1087
- )
1088
- return {row["date"]: int(row["count"]) for _, row in counts.iterrows()}
1089
 
 
 
 
 
1090
 
1091
- def get_calendar_bounds(year: int):
1092
- start = pd.Timestamp(f"{year}-01-01")
1093
- end = pd.Timestamp(f"{year}-12-31")
 
 
 
1094
 
1095
- grid_start = start - pd.Timedelta(days=start.dayofweek)
1096
- grid_end = end + pd.Timedelta(days=(6 - end.dayofweek))
 
 
1097
 
1098
- all_days = pd.date_range(grid_start, grid_end, freq="D")
1099
- num_weeks = len(all_days) // 7
1100
- return start, end, grid_start, grid_end, all_days, num_weeks
 
 
 
 
 
1101
 
 
 
 
 
 
 
 
 
 
 
1102
 
1103
- def month_positions_for_year(year: int, grid_start: pd.Timestamp):
1104
- month_starts = pd.date_range(f"{year}-01-01", f"{year}-12-31", freq="MS")
1105
- return [(d.strftime("%b"), int((d - grid_start).days // 7)) for d in month_starts]
1106
 
1107
 
1108
  # ============================================================
@@ -1365,189 +1319,13 @@ def render_community_loaded_banner(
1365
  return render_status_badge(" · ".join(parts))
1366
 
1367
 
1368
- def render_upvotes_idle_banner(
1369
- username,
1370
- selected_year,
1371
- profile: gr.OAuthProfile | None,
1372
- ):
1373
- username = (username or "").strip()
1374
- selected_year = int(selected_year)
1375
- viewer = getattr(profile, "username", None) if profile else None
1376
-
1377
- parts = []
1378
- if viewer:
1379
- parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
1380
- else:
1381
- parts.append("Not logged in")
1382
-
1383
- if not username:
1384
- parts.append("Enter a username to fetch upvotes activity")
1385
- return render_status_badge(" · ".join(parts))
1386
-
1387
- if viewer and username.lower() == viewer.lower():
1388
- parts.append("Fetch your upvotes activity or enter another username")
1389
- parts.append(str(selected_year))
1390
- else:
1391
- parts.append(f"Ready to fetch upvotes activity for <b>{html.escape(username)}</b>")
1392
- parts.append(str(selected_year))
1393
-
1394
- return render_status_badge(" · ".join(parts))
1395
-
1396
-
1397
- def render_upvotes_invalid_banner(username: str, error_text: str | None = None):
1398
- username = (username or "").strip()
1399
- message = error_text or f"Username or org '{username}' was not found."
1400
- return render_status_badge(html.escape(message))
1401
-
1402
-
1403
- def render_upvotes_fetching_banner(
1404
- username,
1405
- selected_year,
1406
- profile: gr.OAuthProfile | None,
1407
- ):
1408
- username = (username or "").strip()
1409
- selected_year = int(selected_year)
1410
- viewer = getattr(profile, "username", None) if profile else None
1411
-
1412
- if not username:
1413
- return render_upvotes_idle_banner("", selected_year, profile)
1414
-
1415
- parts = []
1416
- if viewer:
1417
- parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
1418
- else:
1419
- parts.append("Not logged in")
1420
-
1421
- if viewer and username.lower() == viewer.lower():
1422
- parts.append("Fetching <b>your upvotes activity</b>")
1423
- else:
1424
- parts.append(f"Fetching upvotes activity for <b>{html.escape(username)}</b>")
1425
-
1426
- parts.append(str(selected_year))
1427
- return render_status_badge(" · ".join(parts))
1428
-
1429
-
1430
- def render_upvotes_loaded_banner(
1431
- upvotes_state: dict | None,
1432
- profile: gr.OAuthProfile | None,
1433
- ):
1434
- if not upvotes_state or not upvotes_state.get("username"):
1435
- year = upvotes_state.get("selected_year", datetime.now().year) if upvotes_state else datetime.now().year
1436
- return render_upvotes_idle_banner("", year, profile)
1437
-
1438
- username = upvotes_state["username"]
1439
- selected_year = int(upvotes_state["selected_year"])
1440
- viewer = getattr(profile, "username", None) if profile else None
1441
-
1442
- parts = []
1443
- if viewer:
1444
- parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
1445
- else:
1446
- parts.append("Not logged in")
1447
-
1448
- if viewer and username.lower() == viewer.lower():
1449
- parts.append("Viewing <b>your upvotes activity</b>")
1450
- else:
1451
- parts.append(f"Viewing upvotes activity for <b>{html.escape(username)}</b>")
1452
-
1453
- parts.append(str(selected_year))
1454
- return render_status_badge(" · ".join(parts))
1455
-
1456
-
1457
- def render_likes_idle_banner(
1458
- username,
1459
- selected_year,
1460
- profile: gr.OAuthProfile | None,
1461
- ):
1462
- username = (username or "").strip()
1463
- selected_year = int(selected_year)
1464
- viewer = getattr(profile, "username", None) if profile else None
1465
-
1466
- parts = []
1467
- if viewer:
1468
- parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
1469
- else:
1470
- parts.append("Not logged in")
1471
-
1472
- if not username:
1473
- parts.append("Enter a username to fetch likes activity")
1474
- return render_status_badge(" · ".join(parts))
1475
-
1476
- if viewer and username.lower() == viewer.lower():
1477
- parts.append("Fetch your likes activity or enter another username")
1478
- parts.append(str(selected_year))
1479
- else:
1480
- parts.append(f"Ready to fetch likes activity for <b>{html.escape(username)}</b>")
1481
- parts.append(str(selected_year))
1482
-
1483
- return render_status_badge(" · ".join(parts))
1484
-
1485
-
1486
- def render_likes_invalid_banner(username: str, error_text: str | None = None):
1487
- username = (username or "").strip()
1488
- message = error_text or f"Username or org '{username}' was not found."
1489
- return render_status_badge(html.escape(message))
1490
-
1491
-
1492
- def render_likes_fetching_banner(
1493
- username,
1494
- selected_year,
1495
- profile: gr.OAuthProfile | None,
1496
- ):
1497
- username = (username or "").strip()
1498
- selected_year = int(selected_year)
1499
- viewer = getattr(profile, "username", None) if profile else None
1500
-
1501
- if not username:
1502
- return render_likes_idle_banner("", selected_year, profile)
1503
-
1504
- parts = []
1505
- if viewer:
1506
- parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
1507
- else:
1508
- parts.append("Not logged in")
1509
-
1510
- if viewer and username.lower() == viewer.lower():
1511
- parts.append("Fetching <b>your likes activity</b>")
1512
- else:
1513
- parts.append(f"Fetching likes activity for <b>{html.escape(username)}</b>")
1514
-
1515
- parts.append(str(selected_year))
1516
- return render_status_badge(" · ".join(parts))
1517
-
1518
-
1519
- def render_likes_loaded_banner(
1520
- likes_state: dict | None,
1521
- profile: gr.OAuthProfile | None,
1522
- ):
1523
- if not likes_state or not likes_state.get("username"):
1524
- year = likes_state.get("selected_year", datetime.now().year) if likes_state else datetime.now().year
1525
- return render_likes_idle_banner("", year, profile)
1526
-
1527
- username = likes_state["username"]
1528
- selected_year = int(likes_state["selected_year"])
1529
- viewer = getattr(profile, "username", None) if profile else None
1530
-
1531
- parts = []
1532
- if viewer:
1533
- parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
1534
- else:
1535
- parts.append("Not logged in")
1536
-
1537
- if viewer and username.lower() == viewer.lower():
1538
- parts.append("Viewing <b>your likes activity</b>")
1539
- else:
1540
- parts.append(f"Viewing likes activity for <b>{html.escape(username)}</b>")
1541
-
1542
- parts.append(str(selected_year))
1543
- return render_status_badge(" · ".join(parts))
1544
-
1545
-
1546
- def render_papers_idle_banner(
1547
  username,
1548
  selected_year,
1549
  profile: gr.OAuthProfile | None,
1550
  ):
 
1551
  username = (username or "").strip()
1552
  selected_year = int(selected_year)
1553
  viewer = getattr(profile, "username", None) if profile else None
@@ -1559,36 +1337,39 @@ def render_papers_idle_banner(
1559
  parts.append("Not logged in")
1560
 
1561
  if not username:
1562
- parts.append("Enter a username to fetch papers activity")
1563
  return render_status_badge(" · ".join(parts))
1564
 
1565
  if viewer and username.lower() == viewer.lower():
1566
- parts.append("Fetch your papers activity or enter another username")
1567
  parts.append(str(selected_year))
1568
  else:
1569
- parts.append(f"Ready to fetch papers activity for <b>{html.escape(username)}</b>")
1570
  parts.append(str(selected_year))
1571
 
1572
  return render_status_badge(" · ".join(parts))
1573
 
1574
 
1575
- def render_papers_invalid_banner(username: str, error_text: str | None = None):
 
1576
  username = (username or "").strip()
1577
  message = error_text or f"Username or org '{username}' was not found."
1578
  return render_status_badge(html.escape(message))
1579
 
1580
 
1581
- def render_papers_fetching_banner(
 
1582
  username,
1583
  selected_year,
1584
  profile: gr.OAuthProfile | None,
1585
  ):
 
1586
  username = (username or "").strip()
1587
  selected_year = int(selected_year)
1588
  viewer = getattr(profile, "username", None) if profile else None
1589
 
1590
  if not username:
1591
- return render_papers_idle_banner("", selected_year, profile)
1592
 
1593
  parts = []
1594
  if viewer:
@@ -1597,24 +1378,26 @@ def render_papers_fetching_banner(
1597
  parts.append("Not logged in")
1598
 
1599
  if viewer and username.lower() == viewer.lower():
1600
- parts.append("Fetching <b>your papers activity</b>")
1601
  else:
1602
- parts.append(f"Fetching papers activity for <b>{html.escape(username)}</b>")
1603
 
1604
  parts.append(str(selected_year))
1605
  return render_status_badge(" · ".join(parts))
1606
 
1607
 
1608
- def render_papers_loaded_banner(
1609
- papers_state: dict | None,
 
1610
  profile: gr.OAuthProfile | None,
1611
  ):
1612
- if not papers_state or not papers_state.get("username"):
1613
- year = papers_state.get("selected_year", datetime.now().year) if papers_state else datetime.now().year
1614
- return render_papers_idle_banner("", year, profile)
 
1615
 
1616
- username = papers_state["username"]
1617
- selected_year = int(papers_state["selected_year"])
1618
  viewer = getattr(profile, "username", None) if profile else None
1619
 
1620
  parts = []
@@ -1624,9 +1407,9 @@ def render_papers_loaded_banner(
1624
  parts.append("Not logged in")
1625
 
1626
  if viewer and username.lower() == viewer.lower():
1627
- parts.append("Viewing <b>your papers activity</b>")
1628
  else:
1629
- parts.append(f"Viewing papers activity for <b>{html.escape(username)}</b>")
1630
 
1631
  parts.append(str(selected_year))
1632
  return render_status_badge(" · ".join(parts))
@@ -1784,129 +1567,68 @@ def render_pr_combined_chart(community_state: dict | None, mode: str, username:
1784
  "opened": f"{len(opened)} PRs opened by {username} in {year}",
1785
  "activity": f"{len(activity)} PR follow-up events for {username} in {year}",
1786
  "both": f"{len(opened) + len(activity)} total PR events for {username} in {year}",
1787
- }[mode]
1788
-
1789
- return render_contrib_chart(
1790
- df,
1791
- year,
1792
- "Pull Requests",
1793
- palette_name="purple",
1794
- subtitle=subtitle,
1795
- )
1796
-
1797
-
1798
- def render_discussion_combined_chart(community_state: dict | None, mode: str, username: str, year: int):
1799
- if not community_state or not community_state.get("fetched"):
1800
- return render_empty_chart("Community Discussions")
1801
-
1802
- opened = community_state["thread_opened"]["dates"]
1803
- activity = community_state["discussion_activity"]["dates"]
1804
- merged = merge_dates(opened, activity, mode)
1805
- df = pd.DataFrame(merged, columns=["date"])
1806
-
1807
- subtitle = {
1808
- "opened": f"{len(opened)} threads opened by {username} in {year}",
1809
- "activity": f"{len(activity)} discussion follow-up events for {username} in {year}",
1810
- "both": f"{len(opened) + len(activity)} total discussion events for {username} in {year}",
1811
- }[mode]
1812
-
1813
- return render_contrib_chart(
1814
- df,
1815
- year,
1816
- "Community Discussions",
1817
- palette_name="teal",
1818
- subtitle=subtitle,
1819
- )
1820
-
1821
-
1822
- def render_upvotes_chart(upvotes_state: dict | None, mode: str, username: str, year: int):
1823
- if not upvotes_state or not upvotes_state.get("fetched"):
1824
- return render_empty_chart("Upvotes")
1825
-
1826
- papers = upvotes_state["papers"]["dates"]
1827
- blog = upvotes_state["blog"]["dates"]
1828
- changelog = upvotes_state["changelog"]["dates"]
1829
-
1830
- if mode == "papers":
1831
- merged = list(papers)
1832
- subtitle = f"{len(papers)} paper upvote(s) by {username} in {year}"
1833
- elif mode == "blog":
1834
- merged = list(blog)
1835
- subtitle = f"{len(blog)} blog upvote(s) by {username} in {year}"
1836
- elif mode == "changelog":
1837
- merged = list(changelog)
1838
- subtitle = f"{len(changelog)} changelog upvote(s) by {username} in {year}"
1839
- else:
1840
- merged = list(papers) + list(blog) + list(changelog)
1841
- subtitle = f"{len(merged)} total upvote event(s) by {username} in {year}"
1842
-
1843
- df = pd.DataFrame(merged, columns=["date"])
1844
 
1845
  return render_contrib_chart(
1846
  df,
1847
  year,
1848
- "Upvotes",
1849
  palette_name="purple",
1850
  subtitle=subtitle,
1851
  )
1852
 
1853
 
1854
- def render_likes_chart(likes_state: dict | None, mode: str, username: str, year: int):
1855
- if not likes_state or not likes_state.get("fetched"):
1856
- return render_empty_chart("Likes")
1857
-
1858
- models = likes_state["models"]["dates"]
1859
- datasets = likes_state["datasets"]["dates"]
1860
- spaces = likes_state["spaces"]["dates"]
1861
-
1862
- if mode == "models":
1863
- merged = list(models)
1864
- subtitle = f"{len(models)} model like(s) by {username} in {year}"
1865
- elif mode == "datasets":
1866
- merged = list(datasets)
1867
- subtitle = f"{len(datasets)} dataset like(s) by {username} in {year}"
1868
- elif mode == "spaces":
1869
- merged = list(spaces)
1870
- subtitle = f"{len(spaces)} space like(s) by {username} in {year}"
1871
- else:
1872
- merged = list(models) + list(datasets) + list(spaces)
1873
- subtitle = f"{len(merged)} total like event(s) by {username} in {year}"
1874
 
 
 
 
1875
  df = pd.DataFrame(merged, columns=["date"])
1876
 
 
 
 
 
 
 
1877
  return render_contrib_chart(
1878
  df,
1879
  year,
1880
- "Likes",
1881
- palette_name="blue",
1882
  subtitle=subtitle,
1883
  )
1884
 
1885
 
1886
- def render_papers_chart(papers_state: dict | None, mode: str, username: str, year: int):
1887
- if not papers_state or not papers_state.get("fetched"):
1888
- return render_empty_chart("Papers")
1889
 
1890
- daily = papers_state["daily"]["dates"]
1891
- authored = papers_state["authored"]["dates"]
1892
 
1893
- if mode == "daily":
1894
- merged = list(daily)
1895
- subtitle = f"{len(daily)} Daily Papers submission(s) by {username} in {year}"
1896
- elif mode == "authored":
1897
- merged = list(authored)
1898
- subtitle = f"{len(authored)} authored paper(s) by {username} in {year}"
 
 
 
1899
  else:
1900
- merged = list(daily) + list(authored)
1901
- subtitle = f"{len(merged)} total papers event(s) by {username} in {year}"
 
1902
 
1903
  df = pd.DataFrame(merged, columns=["date"])
1904
 
1905
  return render_contrib_chart(
1906
  df,
1907
  year,
1908
- "Papers",
1909
- palette_name="teal",
1910
  subtitle=subtitle,
1911
  )
1912
 
@@ -2340,477 +2062,141 @@ def maybe_reset_community_section(
2340
  )
2341
 
2342
 
2343
- def maybe_reset_upvotes_section(
2344
- current_upvotes_state,
2345
- username,
2346
- selected_year,
2347
- upvotes_mode,
2348
- profile: gr.OAuthProfile | None,
2349
- ):
2350
- username = (username or "").strip()
2351
- selected_year = int(selected_year)
2352
- upvotes_mode = upvotes_mode or "all"
2353
-
2354
- identity = validate_username(username, profile) if username else empty_identity("", "No username provided.")
2355
- if username and not identity.get("exists"):
2356
- state = empty_upvotes_state(selected_year)
2357
- state["username"] = username
2358
- state["selected_year"] = selected_year
2359
- state["logs"] = identity.get("error") or f"Username or org '{username}' was not found."
2360
- return (
2361
- state,
2362
- render_upvotes_invalid_banner(username, identity.get("error")),
2363
- gr.update(value="all", visible="hidden"),
2364
- render_empty_chart("Upvotes"),
2365
- state["logs"],
2366
- )
2367
-
2368
- if not current_upvotes_state:
2369
- state = empty_upvotes_state(selected_year)
2370
- return (
2371
- state,
2372
- render_upvotes_idle_banner(username, selected_year, profile),
2373
- gr.update(value="all", visible="hidden"),
2374
- render_empty_chart("Upvotes"),
2375
- "Upvotes activity not fetched yet.",
2376
- )
2377
-
2378
- previous_username = (current_upvotes_state.get("username") or "").strip()
2379
- previous_year = int(current_upvotes_state.get("selected_year") or selected_year)
2380
-
2381
- same_username = previous_username.lower() == username.lower()
2382
- same_year = previous_year == selected_year
2383
-
2384
- if same_username and same_year and current_upvotes_state.get("fetched"):
2385
- chart = render_upvotes_chart(
2386
- current_upvotes_state,
2387
- upvotes_mode,
2388
- username=username,
2389
- year=selected_year,
2390
- )
2391
- return (
2392
- current_upvotes_state,
2393
- render_upvotes_loaded_banner(current_upvotes_state, profile),
2394
- gr.update(value=upvotes_mode, visible=True),
2395
- chart,
2396
- current_upvotes_state.get("logs", "Upvotes activity already fetched."),
2397
- )
2398
-
2399
- state = empty_upvotes_state(selected_year)
2400
- return (
2401
- state,
2402
- render_upvotes_idle_banner(username, selected_year, profile),
2403
- gr.update(value="all", visible="hidden"),
2404
- render_empty_chart("Upvotes"),
2405
- "Upvotes activity needs to be fetched for the current username/year.",
2406
- )
2407
-
2408
-
2409
- def maybe_reset_likes_section(
2410
- current_likes_state,
2411
- username,
2412
- selected_year,
2413
- likes_mode,
2414
- profile: gr.OAuthProfile | None,
2415
- ):
2416
  username = (username or "").strip()
2417
- selected_year = int(selected_year)
2418
- likes_mode = likes_mode or "all"
2419
-
2420
- identity = validate_username(username, profile) if username else empty_identity("", "No username provided.")
2421
- if username and not identity.get("exists"):
2422
- state = empty_likes_state(selected_year)
2423
- state["username"] = username
2424
- state["selected_year"] = selected_year
2425
- state["logs"] = identity.get("error") or f"Username or org '{username}' was not found."
2426
- return (
2427
- state,
2428
- render_likes_invalid_banner(username, identity.get("error")),
2429
- gr.update(value="all", visible="hidden"),
2430
- render_empty_chart("Likes"),
2431
- state["logs"],
2432
- )
2433
-
2434
- if not current_likes_state:
2435
- state = empty_likes_state(selected_year)
2436
- return (
2437
- state,
2438
- render_likes_idle_banner(username, selected_year, profile),
2439
- gr.update(value="all", visible="hidden"),
2440
- render_empty_chart("Likes"),
2441
- "Likes activity not fetched yet.",
2442
- )
2443
-
2444
- previous_username = (current_likes_state.get("username") or "").strip()
2445
- previous_year = int(current_likes_state.get("selected_year") or selected_year)
2446
-
2447
- same_username = previous_username.lower() == username.lower()
2448
- same_year = previous_year == selected_year
2449
-
2450
- if same_username and same_year and current_likes_state.get("fetched"):
2451
- chart = render_likes_chart(
2452
- current_likes_state,
2453
- likes_mode,
2454
- username=username,
2455
- year=selected_year,
2456
- )
2457
- return (
2458
- current_likes_state,
2459
- render_likes_loaded_banner(current_likes_state, profile),
2460
- gr.update(value=likes_mode, visible=True),
2461
- chart,
2462
- current_likes_state.get("logs", "Likes activity already fetched."),
2463
- )
2464
-
2465
- state = empty_likes_state(selected_year)
2466
- return (
2467
- state,
2468
- render_likes_idle_banner(username, selected_year, profile),
2469
- gr.update(value="all", visible="hidden"),
2470
- render_empty_chart("Likes"),
2471
- "Likes activity needs to be fetched for the current username/year.",
2472
- )
2473
 
2474
 
2475
- def maybe_reset_papers_section(
2476
- current_papers_state,
 
2477
  username,
2478
  selected_year,
2479
- papers_mode,
2480
  profile: gr.OAuthProfile | None,
2481
  ):
 
2482
  username = (username or "").strip()
2483
  selected_year = int(selected_year)
2484
- papers_mode = papers_mode or "all"
2485
 
2486
  identity = validate_username(username, profile) if username else empty_identity("", "No username provided.")
2487
  if username and not identity.get("exists"):
2488
- state = empty_papers_state(selected_year)
2489
  state["username"] = username
2490
  state["selected_year"] = selected_year
2491
  state["logs"] = identity.get("error") or f"Username or org '{username}' was not found."
2492
  return (
2493
  state,
2494
- render_papers_invalid_banner(username, identity.get("error")),
2495
- gr.update(value="all", visible="hidden"),
2496
- render_empty_chart("Papers"),
2497
  state["logs"],
2498
  )
2499
 
2500
- if not current_papers_state:
2501
- state = empty_papers_state(selected_year)
2502
  return (
2503
  state,
2504
- render_papers_idle_banner(username, selected_year, profile),
2505
- gr.update(value="all", visible="hidden"),
2506
- render_empty_chart("Papers"),
2507
- "Papers activity not fetched yet.",
2508
  )
2509
 
2510
- previous_username = (current_papers_state.get("username") or "").strip()
2511
- previous_year = int(current_papers_state.get("selected_year") or selected_year)
2512
 
2513
  same_username = previous_username.lower() == username.lower()
2514
  same_year = previous_year == selected_year
2515
 
2516
- if same_username and same_year and current_papers_state.get("fetched"):
2517
- chart = render_papers_chart(
2518
- current_papers_state,
2519
- papers_mode,
 
2520
  username=username,
2521
  year=selected_year,
2522
  )
2523
  return (
2524
- current_papers_state,
2525
- render_papers_loaded_banner(current_papers_state, profile),
2526
- gr.update(value=papers_mode, visible=True),
2527
  chart,
2528
- current_papers_state.get("logs", "Papers activity already fetched."),
2529
- )
2530
-
2531
- state = empty_papers_state(selected_year)
2532
- return (
2533
- state,
2534
- render_papers_idle_banner(username, selected_year, profile),
2535
- gr.update(value="all", visible="hidden"),
2536
- render_empty_chart("Papers"),
2537
- "Papers activity needs to be fetched for the current username/year.",
2538
- )
2539
-
2540
-
2541
- def prepare_community_fetch_ui(username, selected_year, profile: gr.OAuthProfile | None):
2542
- username = (username or "").strip()
2543
- if username:
2544
- identity = validate_username(username, profile)
2545
- if not identity.get("exists"):
2546
- return render_community_invalid_banner(username, identity.get("error"))
2547
- return render_community_fetching_banner(username, selected_year, profile)
2548
-
2549
-
2550
- def prepare_upvotes_fetch_ui(username, selected_year, profile: gr.OAuthProfile | None):
2551
- username = (username or "").strip()
2552
- if username:
2553
- identity = validate_username(username, profile)
2554
- if not identity.get("exists"):
2555
- return render_upvotes_invalid_banner(username, identity.get("error"))
2556
- return render_upvotes_fetching_banner(username, selected_year, profile)
2557
-
2558
-
2559
- def prepare_likes_fetch_ui(username, selected_year, profile: gr.OAuthProfile | None):
2560
- username = (username or "").strip()
2561
- if username:
2562
- identity = validate_username(username, profile)
2563
- if not identity.get("exists"):
2564
- return render_likes_invalid_banner(username, identity.get("error"))
2565
- return render_likes_fetching_banner(username, selected_year, profile)
2566
-
2567
-
2568
- def prepare_papers_fetch_ui(username, selected_year, profile: gr.OAuthProfile | None):
2569
- username = (username or "").strip()
2570
- if username:
2571
- identity = validate_username(username, profile)
2572
- if not identity.get("exists"):
2573
- return render_papers_invalid_banner(username, identity.get("error"))
2574
- return render_papers_fetching_banner(username, selected_year, profile)
2575
-
2576
-
2577
- def fetch_and_render_community(
2578
- current_state,
2579
- username,
2580
- selected_year,
2581
- pr_mode,
2582
- discussion_mode,
2583
- profile: gr.OAuthProfile | None,
2584
- ):
2585
- username = (username or "").strip()
2586
- selected_year = int(selected_year)
2587
- pr_mode = pr_mode or "both"
2588
- discussion_mode = discussion_mode or "both"
2589
-
2590
- identity = validate_username(username, profile)
2591
- if username and not identity.get("exists"):
2592
- empty_state = empty_community_state(selected_year)
2593
- empty_state["username"] = username
2594
- empty_state["selected_year"] = selected_year
2595
- empty_state["logs"] = identity.get("error") or f"Username or org '{username}' was not found."
2596
- return (
2597
- empty_state,
2598
- render_community_invalid_banner(username, identity.get("error")),
2599
- render_empty_chart("Pull Requests"),
2600
- render_empty_chart("Community Discussions"),
2601
- empty_state["logs"],
2602
- gr.update(value="both", visible="hidden"),
2603
- gr.update(value="both", visible="hidden"),
2604
- )
2605
-
2606
- state = fetch_all_community_activity(username, selected_year)
2607
-
2608
- pr_chart = render_pr_combined_chart(
2609
- state,
2610
- pr_mode,
2611
- username=username,
2612
- year=selected_year,
2613
- )
2614
-
2615
- discussion_chart = render_discussion_combined_chart(
2616
- state,
2617
- discussion_mode,
2618
- username=username,
2619
- year=selected_year,
2620
- )
2621
-
2622
- return (
2623
- state,
2624
- render_community_loaded_banner(state, profile),
2625
- pr_chart,
2626
- discussion_chart,
2627
- state["logs"],
2628
- gr.update(value=pr_mode, visible=True),
2629
- gr.update(value=discussion_mode, visible=True),
2630
- )
2631
-
2632
-
2633
- def fetch_and_render_upvotes(
2634
- current_state,
2635
- username,
2636
- selected_year,
2637
- upvotes_mode,
2638
- profile: gr.OAuthProfile | None,
2639
- ):
2640
- username = (username or "").strip()
2641
- selected_year = int(selected_year)
2642
- upvotes_mode = upvotes_mode or "all"
2643
-
2644
- identity = validate_username(username, profile)
2645
- if username and not identity.get("exists"):
2646
- empty_state = empty_upvotes_state(selected_year)
2647
- empty_state["username"] = username
2648
- empty_state["selected_year"] = selected_year
2649
- empty_state["logs"] = identity.get("error") or f"Username or org '{username}' was not found."
2650
- return (
2651
- empty_state,
2652
- render_upvotes_invalid_banner(username, identity.get("error")),
2653
- render_empty_chart("Upvotes"),
2654
- empty_state["logs"],
2655
- gr.update(value="all", visible="hidden"),
2656
- )
2657
-
2658
- state = fetch_all_upvotes_activity(username, selected_year)
2659
-
2660
- chart = render_upvotes_chart(
2661
- state,
2662
- upvotes_mode,
2663
- username=username,
2664
- year=selected_year,
2665
- )
2666
-
2667
- return (
2668
- state,
2669
- render_upvotes_loaded_banner(state, profile),
2670
- chart,
2671
- state["logs"],
2672
- gr.update(value=upvotes_mode, visible=True),
2673
- )
2674
-
2675
-
2676
- def fetch_and_render_likes(
2677
- current_state,
2678
- username,
2679
- selected_year,
2680
- likes_mode,
2681
- profile: gr.OAuthProfile | None,
2682
- ):
2683
- username = (username or "").strip()
2684
- selected_year = int(selected_year)
2685
- likes_mode = likes_mode or "all"
2686
-
2687
- identity = validate_username(username, profile)
2688
- if username and not identity.get("exists"):
2689
- empty_state = empty_likes_state(selected_year)
2690
- empty_state["username"] = username
2691
- empty_state["selected_year"] = selected_year
2692
- empty_state["logs"] = identity.get("error") or f"Username or org '{username}' was not found."
2693
- return (
2694
- empty_state,
2695
- render_likes_invalid_banner(username, identity.get("error")),
2696
- render_empty_chart("Likes"),
2697
- empty_state["logs"],
2698
- gr.update(value="all", visible="hidden"),
2699
  )
2700
 
2701
- state = fetch_all_likes_activity(username, selected_year)
2702
-
2703
- chart = render_likes_chart(
2704
- state,
2705
- likes_mode,
2706
- username=username,
2707
- year=selected_year,
2708
- )
2709
-
2710
  return (
2711
  state,
2712
- render_likes_loaded_banner(state, profile),
2713
- chart,
2714
- state["logs"],
2715
- gr.update(value=likes_mode, visible=True),
2716
  )
2717
 
2718
 
2719
- def fetch_and_render_papers(
 
2720
  current_state,
2721
  username,
2722
  selected_year,
2723
- papers_mode,
2724
  profile: gr.OAuthProfile | None,
2725
  ):
 
 
2726
  username = (username or "").strip()
2727
  selected_year = int(selected_year)
2728
- papers_mode = papers_mode or "all"
2729
 
2730
  identity = validate_username(username, profile)
2731
  if username and not identity.get("exists"):
2732
- empty_state = empty_papers_state(selected_year)
2733
  empty_state["username"] = username
2734
  empty_state["selected_year"] = selected_year
2735
  empty_state["logs"] = identity.get("error") or f"Username or org '{username}' was not found."
2736
  return (
2737
  empty_state,
2738
- render_papers_invalid_banner(username, identity.get("error")),
2739
- render_empty_chart("Papers"),
2740
  empty_state["logs"],
2741
- gr.update(value="all", visible="hidden"),
2742
  )
2743
 
2744
- state = fetch_all_papers_activity(username, selected_year)
2745
 
2746
- chart = render_papers_chart(
 
2747
  state,
2748
- papers_mode,
2749
  username=username,
2750
  year=selected_year,
2751
  )
2752
 
2753
  return (
2754
  state,
2755
- render_papers_loaded_banner(state, profile),
2756
  chart,
2757
  state["logs"],
2758
- gr.update(value=papers_mode, visible=True),
2759
- )
2760
-
2761
-
2762
- def rerender_pr_from_state(community_state, pr_mode):
2763
- if not community_state:
2764
- return render_empty_chart("Pull Requests")
2765
- return render_pr_combined_chart(
2766
- community_state,
2767
- pr_mode or "both",
2768
- username=(community_state.get("username") or ""),
2769
- year=int(community_state.get("selected_year") or datetime.now().year),
2770
- )
2771
-
2772
-
2773
- def rerender_discussions_from_state(community_state, discussion_mode):
2774
- if not community_state:
2775
- return render_empty_chart("Community Discussions")
2776
- return render_discussion_combined_chart(
2777
- community_state,
2778
- discussion_mode or "both",
2779
- username=(community_state.get("username") or ""),
2780
- year=int(community_state.get("selected_year") or datetime.now().year),
2781
- )
2782
-
2783
-
2784
- def rerender_upvotes_from_state(upvotes_state, upvotes_mode):
2785
- if not upvotes_state:
2786
- return render_empty_chart("Upvotes")
2787
- return render_upvotes_chart(
2788
- upvotes_state,
2789
- upvotes_mode or "all",
2790
- username=(upvotes_state.get("username") or ""),
2791
- year=int(upvotes_state.get("selected_year") or datetime.now().year),
2792
- )
2793
-
2794
-
2795
- def rerender_likes_from_state(likes_state, likes_mode):
2796
- if not likes_state:
2797
- return render_empty_chart("Likes")
2798
- return render_likes_chart(
2799
- likes_state,
2800
- likes_mode or "all",
2801
- username=(likes_state.get("username") or ""),
2802
- year=int(likes_state.get("selected_year") or datetime.now().year),
2803
  )
2804
 
2805
 
2806
- def rerender_papers_from_state(papers_state, papers_mode):
2807
- if not papers_state:
2808
- return render_empty_chart("Papers")
2809
- return render_papers_chart(
2810
- papers_state,
2811
- papers_mode or "all",
2812
- username=(papers_state.get("username") or ""),
2813
- year=int(papers_state.get("selected_year") or datetime.now().year),
 
 
2814
  )
2815
 
2816
 
@@ -2832,9 +2218,9 @@ def hydrate_header_on_load(
2832
 
2833
  repo_context_html = render_idle_context_banner(effective_username, profile)
2834
  community_context_html = render_community_idle_banner(effective_username, selected_year, profile)
2835
- upvotes_context_html = render_upvotes_idle_banner(effective_username, selected_year, profile)
2836
- likes_context_html = render_likes_idle_banner(effective_username, selected_year, profile)
2837
- papers_context_html = render_papers_idle_banner(effective_username, selected_year, profile)
2838
 
2839
  return (
2840
  username_update,
@@ -3499,26 +2885,21 @@ with gr.Blocks(title="HF Contributions") as demo:
3499
 
3500
  with gr.Column(elem_classes="toggles-container"):
3501
  upvotes_view_mode = gr.Radio(
3502
- choices=[
3503
- ("All", "all"),
3504
- ("Papers", "papers"),
3505
- ("Blog", "blog"),
3506
- ("Changelog", "changelog"),
3507
- ],
3508
- value="all",
3509
- label="Upvotes view",
3510
  visible="hidden",
3511
  elem_classes=["gh-toggle-radio"],
3512
  elem_id="upvotes-view-toggle",
3513
  )
3514
 
3515
- upvotes_chart = gr.HTML(render_empty_chart("Upvotes"))
3516
 
3517
  upvotes_logs_box = gr.Textbox(
3518
  label="Upvotes Fetch Logs",
3519
  interactive=False,
3520
  lines=10,
3521
- value="Upvotes activity not fetched yet.",
3522
  )
3523
 
3524
  gr.Markdown("## Likes activity")
@@ -3530,26 +2911,21 @@ with gr.Blocks(title="HF Contributions") as demo:
3530
 
3531
  with gr.Column(elem_classes="toggles-container"):
3532
  likes_view_mode = gr.Radio(
3533
- choices=[
3534
- ("All", "all"),
3535
- ("Models", "models"),
3536
- ("Datasets", "datasets"),
3537
- ("Spaces", "spaces"),
3538
- ],
3539
- value="all",
3540
- label="Likes view",
3541
  visible="hidden",
3542
  elem_classes=["gh-toggle-radio"],
3543
  elem_id="likes-view-toggle",
3544
  )
3545
 
3546
- likes_chart = gr.HTML(render_empty_chart("Likes"))
3547
 
3548
  likes_logs_box = gr.Textbox(
3549
  label="Likes Fetch Logs",
3550
  interactive=False,
3551
  lines=10,
3552
- value="Likes activity not fetched yet.",
3553
  )
3554
 
3555
  gr.Markdown("## Papers activity")
@@ -3561,25 +2937,21 @@ with gr.Blocks(title="HF Contributions") as demo:
3561
 
3562
  with gr.Column(elem_classes="toggles-container"):
3563
  papers_view_mode = gr.Radio(
3564
- choices=[
3565
- ("All", "all"),
3566
- ("Daily Papers", "daily"),
3567
- ("Authored", "authored"),
3568
- ],
3569
- value="all",
3570
- label="Papers view",
3571
  visible="hidden",
3572
  elem_classes=["gh-toggle-radio"],
3573
  elem_id="papers-view-toggle",
3574
  )
3575
 
3576
- papers_chart = gr.HTML(render_empty_chart("Papers"))
3577
 
3578
  papers_logs_box = gr.Textbox(
3579
  label="Papers Fetch Logs",
3580
  interactive=False,
3581
  lines=10,
3582
- value="Papers activity not fetched yet.",
3583
  )
3584
 
3585
  repo_fetch_outputs = [
@@ -3677,7 +3049,14 @@ with gr.Blocks(title="HF Contributions") as demo:
3677
  demo.load(
3678
  fn=hydrate_header_on_load,
3679
  inputs=[username, selected_year],
3680
- outputs=[username, context_box, community_context_box, upvotes_context_box, likes_context_box, papers_context_box],
 
 
 
 
 
 
 
3681
  show_progress="hidden",
3682
  )
3683
 
@@ -3705,17 +3084,17 @@ with gr.Blocks(title="HF Contributions") as demo:
3705
  outputs=community_reset_outputs,
3706
  show_progress="hidden",
3707
  ).then(
3708
- fn=maybe_reset_upvotes_section,
3709
  inputs=[upvotes_state, username, selected_year, upvotes_view_mode],
3710
  outputs=upvotes_reset_outputs,
3711
  show_progress="hidden",
3712
  ).then(
3713
- fn=maybe_reset_likes_section,
3714
  inputs=[likes_state, username, selected_year, likes_view_mode],
3715
  outputs=likes_reset_outputs,
3716
  show_progress="hidden",
3717
  ).then(
3718
- fn=maybe_reset_papers_section,
3719
  inputs=[papers_state, username, selected_year, papers_view_mode],
3720
  outputs=papers_reset_outputs,
3721
  show_progress="hidden",
@@ -3760,12 +3139,12 @@ with gr.Blocks(title="HF Contributions") as demo:
3760
  )
3761
 
3762
  fetch_upvotes_button.click(
3763
- fn=prepare_upvotes_fetch_ui,
3764
  inputs=[username, selected_year],
3765
  outputs=upvotes_context_box,
3766
  show_progress="hidden",
3767
  ).then(
3768
- fn=fetch_and_render_upvotes,
3769
  inputs=[upvotes_state, username, selected_year, upvotes_view_mode],
3770
  outputs=upvotes_fetch_outputs,
3771
  show_progress="full",
@@ -3776,19 +3155,19 @@ with gr.Blocks(title="HF Contributions") as demo:
3776
  )
3777
 
3778
  upvotes_view_mode.change(
3779
- fn=rerender_upvotes_from_state,
3780
  inputs=[upvotes_state, upvotes_view_mode],
3781
  outputs=upvotes_chart,
3782
  show_progress="hidden",
3783
  )
3784
 
3785
  fetch_likes_button.click(
3786
- fn=prepare_likes_fetch_ui,
3787
  inputs=[username, selected_year],
3788
  outputs=likes_context_box,
3789
  show_progress="hidden",
3790
  ).then(
3791
- fn=fetch_and_render_likes,
3792
  inputs=[likes_state, username, selected_year, likes_view_mode],
3793
  outputs=likes_fetch_outputs,
3794
  show_progress="full",
@@ -3799,19 +3178,19 @@ with gr.Blocks(title="HF Contributions") as demo:
3799
  )
3800
 
3801
  likes_view_mode.change(
3802
- fn=rerender_likes_from_state,
3803
  inputs=[likes_state, likes_view_mode],
3804
  outputs=likes_chart,
3805
  show_progress="hidden",
3806
  )
3807
 
3808
  fetch_papers_button.click(
3809
- fn=prepare_papers_fetch_ui,
3810
  inputs=[username, selected_year],
3811
  outputs=papers_context_box,
3812
  show_progress="hidden",
3813
  ).then(
3814
- fn=fetch_and_render_papers,
3815
  inputs=[papers_state, username, selected_year, papers_view_mode],
3816
  outputs=papers_fetch_outputs,
3817
  show_progress="full",
@@ -3822,7 +3201,7 @@ with gr.Blocks(title="HF Contributions") as demo:
3822
  )
3823
 
3824
  papers_view_mode.change(
3825
- fn=rerender_papers_from_state,
3826
  inputs=[papers_state, papers_view_mode],
3827
  outputs=papers_chart,
3828
  show_progress="hidden",
 
363
  }
364
 
365
 
366
+ def empty_bucketed_activity_state(
367
+ selected_year: int,
368
+ logs_text: str,
369
+ bucket_titles: dict[str, str],
370
+ ):
371
+ state = {
372
  "username": "",
373
  "selected_year": int(selected_year),
374
  "fetched": False,
375
+ "logs": logs_text,
 
 
 
 
376
  }
377
 
378
+ for key, title in bucket_titles.items():
379
+ state[key] = empty_activity_bucket(title)
380
 
381
+ return state
382
+
383
+
384
+ def build_fetched_bucket(title: str, dates: list, selected_year: int, label: str):
385
+ return {
386
+ "title": title,
387
+ "fetched": True,
388
+ "dates": dates,
389
+ "count": len(dates),
390
+ "logs": f"Found {len(dates)} {label} in {selected_year}.",
391
+ }
392
+
393
+
394
+ def build_failed_bucket(title: str):
395
+ return {
396
+ "title": title,
397
+ "fetched": True,
398
+ "dates": [],
399
+ "count": 0,
400
+ "logs": "",
401
+ }
402
+
403
+
404
+ # ============================================================
405
+ # Bucketed sections config
406
+ # ============================================================
407
+ BUCKETED_SECTION_CONFIGS = {
408
+ "upvotes": {
409
+ "section_title": "Upvotes activity",
410
+ "chart_title": "Upvotes",
411
+ "palette_name": "purple",
412
+ "context_subject_label": "upvotes activity",
413
+ "radio_label": "Upvotes view",
414
+ "default_mode": "all",
415
+ "mode_labels": {
416
+ "all": "All",
417
+ "papers": "Papers",
418
+ "blog": "Blog",
419
+ "changelog": "Changelog",
420
+ },
421
+ "bucket_titles": {
422
+ "papers": "Paper Upvotes",
423
+ "blog": "Blog Upvotes",
424
+ "changelog": "Changelog Upvotes",
425
+ },
426
+ "bucket_log_labels": {
427
+ "papers": "paper upvote(s)",
428
+ "blog": "blog upvote(s)",
429
+ "changelog": "changelog upvote(s)",
430
+ },
431
+ "empty_logs_text": "Upvotes activity not fetched yet.",
432
+ },
433
+ "likes": {
434
+ "section_title": "Likes activity",
435
+ "chart_title": "Likes",
436
+ "palette_name": "blue",
437
+ "context_subject_label": "likes activity",
438
+ "radio_label": "Likes view",
439
+ "default_mode": "all",
440
+ "mode_labels": {
441
+ "all": "All",
442
+ "models": "Models",
443
+ "datasets": "Datasets",
444
+ "spaces": "Spaces",
445
+ },
446
+ "bucket_titles": {
447
+ "models": "Model Likes",
448
+ "datasets": "Dataset Likes",
449
+ "spaces": "Space Likes",
450
+ },
451
+ "bucket_log_labels": {
452
+ "models": "model like(s)",
453
+ "datasets": "dataset like(s)",
454
+ "spaces": "space like(s)",
455
+ },
456
+ "empty_logs_text": "Likes activity not fetched yet.",
457
+ },
458
+ "papers": {
459
+ "section_title": "Papers activity",
460
+ "chart_title": "Papers",
461
+ "palette_name": "teal",
462
+ "context_subject_label": "papers activity",
463
+ "radio_label": "Papers view",
464
+ "default_mode": "all",
465
+ "mode_labels": {
466
+ "all": "All",
467
+ "daily": "Daily Papers",
468
+ "authored": "Authored",
469
+ },
470
+ "bucket_titles": {
471
+ "daily": "Daily Papers",
472
+ "authored": "Authored Papers",
473
+ },
474
+ "bucket_log_labels": {
475
+ "daily": "daily paper submission(s)",
476
+ "authored": "authored paper(s)",
477
+ },
478
+ "empty_logs_text": "Papers activity not fetched yet.",
479
+ },
480
+ }
481
+
482
+
483
+ def get_bucketed_cfg(section_key: str) -> dict:
484
+ return BUCKETED_SECTION_CONFIGS[section_key]
485
+
486
+
487
+ # ============================================================
488
+ # Generic bucketed recent-activity fetch helpers
489
+ # ============================================================
490
+ def fetch_bucketed_recent_activity(
491
  username: str,
492
  selected_year: int,
493
+ cache_scope: str,
494
+ activity_type: str,
495
+ bucket_titles: dict[str, str],
496
+ bucket_log_labels: dict[str, str],
497
+ empty_logs_text: str,
498
+ classifier,
499
  ):
500
  username = (username or "").strip()
501
  selected_year = int(selected_year)
502
 
503
+ cache_key = (username.lower(), selected_year, cache_scope)
504
+ cached = get_cache(cache_key)
505
+ if cached:
506
+ return cached
 
 
 
507
 
508
  if not username:
509
+ state = empty_bucketed_activity_state(selected_year, empty_logs_text, bucket_titles)
510
+ state["fetched"] = True
511
+ state["logs"] = "No username provided."
512
+ set_cache(cache_key, state)
513
+ return state
 
 
514
 
515
+ bucket_dates = {key: [] for key in bucket_titles}
516
+ bucket_seen_ids = {key: set() for key in bucket_titles}
517
+
518
+ cursor = None
519
  skip = 0
520
  limit = 20
 
521
  pages_fetched = 0
522
 
523
  MAX_PAGES = 40
524
+ MAX_DURATION = 20
 
525
  start = time.time()
526
 
527
  logs = []
 
535
  }
536
  )
537
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  try:
539
  while True:
540
  if time.time() - start > MAX_DURATION:
 
549
 
550
  params = {
551
  "limit": limit,
552
+ "activityType": activity_type,
553
  "feedType": "user",
554
  "entity": username,
555
  }
 
568
  payload = response.json()
569
  pages_fetched += 1
570
 
571
+ events = payload.get("recentActivity", [])
572
+ next_cursor = payload.get("cursor")
573
 
574
  if not events:
575
  logs.append("No more events.")
 
581
  if not isinstance(event, dict):
582
  continue
583
 
584
+ raw_time = event.get("time")
585
+ if not raw_time:
586
+ continue
587
+
588
+ try:
589
+ event_ts = pd.to_datetime(raw_time, utc=True)
590
+ except Exception:
591
+ continue
592
+
593
+ if event_ts.year < selected_year:
594
  stop_early = True
595
  break
596
 
597
+ if event_ts.year != selected_year:
 
598
  continue
599
 
600
+ bucket_key, unique_id = classifier(event)
601
+
602
+ if not bucket_key or not unique_id:
603
  continue
604
 
605
+ if bucket_key not in bucket_dates:
 
606
  continue
607
 
608
+ if unique_id in bucket_seen_ids[bucket_key]:
609
  continue
610
 
611
+ bucket_seen_ids[bucket_key].add(unique_id)
612
+ bucket_dates[bucket_key].append(event_ts.tz_localize(None).date())
613
 
614
  if stop_early:
615
  logs.append("Stopped early after reaching feed events older than selected year.")
 
620
  else:
621
  skip += limit
622
 
623
+ result = {
624
+ "username": username,
625
+ "selected_year": selected_year,
 
 
626
  "fetched": True,
627
+ "logs": "",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
628
  }
629
 
630
  for key, title in bucket_titles.items():
 
724
  return bucket_key, unique_id
725
 
726
 
727
+ BUCKETED_FETCH_HANDLERS = {
728
+ "upvotes": {
729
+ "activity_type": "upvote",
730
+ "classifier": classify_upvote_event,
731
+ },
732
+ "likes": {
733
+ "activity_type": "like",
734
+ "classifier": classify_like_event,
735
+ },
736
+ "papers": {
737
+ "activity_type": "update-paper",
738
+ "classifier": classify_paper_event,
739
+ },
740
+ }
741
+
742
+
743
+ def empty_generic_section_state(section_key: str, selected_year: int):
744
+ cfg = get_bucketed_cfg(section_key)
745
  return empty_bucketed_activity_state(
746
  selected_year=selected_year,
747
+ logs_text=cfg["empty_logs_text"],
748
+ bucket_titles=cfg["bucket_titles"],
 
 
 
 
749
  )
750
 
751
 
752
+ def fetch_generic_bucketed_section(section_key: str, username: str, selected_year: int):
753
+ cfg = get_bucketed_cfg(section_key)
754
+ handler = BUCKETED_FETCH_HANDLERS[section_key]
755
  return fetch_bucketed_recent_activity(
756
  username=username,
757
  selected_year=selected_year,
758
+ cache_scope=section_key,
759
+ activity_type=handler["activity_type"],
760
+ bucket_titles=cfg["bucket_titles"],
761
+ bucket_log_labels=cfg["bucket_log_labels"],
762
+ empty_logs_text=cfg["empty_logs_text"],
763
+ classifier=handler["classifier"],
 
 
 
 
 
 
 
 
764
  )
765
 
766
 
767
  # ============================================================
768
+ # Community activity fetch
769
  # ============================================================
770
+ def empty_community_state(selected_year: int):
771
+ return {
772
+ "username": "",
773
+ "selected_year": int(selected_year),
774
+ "fetched": False,
775
+ "pr_opened": empty_activity_bucket("Pull Requests"),
776
+ "pr_activity": empty_activity_bucket("PR Activity"),
777
+ "thread_opened": empty_activity_bucket("Community Threads"),
778
+ "discussion_activity": empty_activity_bucket("Community Activity"),
779
+ "logs": "Community activity not fetched yet.",
780
+ }
781
 
782
 
783
+ def fetch_user_discussion_activity(
784
+ username: str,
785
+ selected_year: int,
786
+ mode: str,
787
+ ):
788
+ username = (username or "").strip()
789
+ selected_year = int(selected_year)
 
 
 
 
 
 
 
 
 
 
 
 
790
 
791
+ title_map = {
792
+ "pr_opened": "Pull Requests",
793
+ "pr_activity": "PR Activity",
794
+ "thread_opened": "Community Threads",
795
+ "discussion_activity": "Community Activity",
796
+ }
797
+ title = title_map.get(mode, "Community")
798
 
799
+ if not username:
800
+ return {
801
+ "title": title,
802
+ "fetched": True,
803
+ "dates": [],
804
+ "count": 0,
805
+ "logs": "No username provided.",
806
+ }
807
+
808
+ dates = []
809
+ seen_ids = set()
810
+ skip = 0
811
+ limit = 20
812
+ cursor = None
813
+ pages_fetched = 0
814
+
815
+ MAX_PAGES = 40
816
+ MAX_DURATION = 25
817
+ OPEN_EVENT_EPSILON_SECONDS = 90
818
+ start = time.time()
819
+
820
+ logs = []
821
+
822
+ session = requests.Session()
823
+ session.headers.update(
824
+ {
825
+ "User-Agent": "hf-contributions-app/1.0",
826
+ "Accept": "application/json",
827
+ "Cache-Control": "no-cache",
828
+ }
829
  )
830
 
831
+ def normalize_payload(payload):
832
+ if isinstance(payload, list):
833
+ return payload, None
834
+
835
+ if isinstance(payload, dict):
836
+ events = payload.get("recentActivity")
837
+ if isinstance(events, list):
838
+ return events, payload.get("cursor")
839
+
840
+ for key in ["items", "events", "activity"]:
841
+ value = payload.get(key)
842
+ if isinstance(value, list):
843
+ return value, payload.get("cursor")
844
+
845
+ return [], None
846
+
847
+ def extract_author_name(discussion: dict) -> str | None:
848
+ author = discussion.get("author")
849
+ if isinstance(author, str):
850
+ return author
851
+ if isinstance(author, dict):
852
+ for key in ["name", "username", "login"]:
853
+ value = author.get(key)
854
+ if isinstance(value, str):
855
+ return value
856
+ return None
857
+
858
+ def parse_ts(raw_value):
859
+ if not raw_value:
860
+ return None
861
+ try:
862
+ return pd.to_datetime(raw_value, utc=True)
863
+ except Exception:
864
+ return None
865
+
866
+ def event_matches_mode(event: dict, discussion: dict) -> tuple[bool, str | None]:
867
+ is_pr = bool(discussion.get("isPullRequest", False))
868
+ author_name = extract_author_name(discussion)
869
+ repo_id = event.get("repoId", "unknown-repo")
870
+ discussion_num = discussion.get("num", "unknown-num")
871
+ event_id = event.get("eventId", "unknown-event")
872
+
873
+ if mode == "pr_opened":
874
+ if not is_pr:
875
+ return False, None
876
+ if not author_name or author_name.lower() != username.lower():
877
+ return False, None
878
+ return True, f"{repo_id}::pr::{discussion_num}"
879
+
880
+ if mode == "pr_activity":
881
+ if not is_pr:
882
+ return False, None
883
+ return True, f"event::{event_id}"
884
+
885
+ if mode == "thread_opened":
886
+ if is_pr:
887
+ return False, None
888
+ if not author_name or author_name.lower() != username.lower():
889
+ return False, None
890
+ return True, f"{repo_id}::thread::{discussion_num}"
891
+
892
+ if mode == "discussion_activity":
893
+ if is_pr:
894
+ return False, None
895
+ return True, f"event::{event_id}"
896
+
897
+ return False, None
898
+
899
+ def extract_date_for_mode(event: dict, discussion: dict):
900
+ event_ts = parse_ts(event.get("time"))
901
+ created_ts = parse_ts(discussion.get("createdAt"))
902
+
903
+ if mode in {"pr_opened", "thread_opened"}:
904
+ if created_ts is None:
905
+ return None
906
+ return created_ts.tz_localize(None).date()
907
+
908
+ if mode in {"pr_activity", "discussion_activity"}:
909
+ if event_ts is None:
910
+ return None
911
+ if created_ts is not None:
912
+ delta_seconds = abs((event_ts - created_ts).total_seconds())
913
+ if delta_seconds <= OPEN_EVENT_EPSILON_SECONDS:
914
+ return None
915
+ return event_ts.tz_localize(None).date()
916
+
917
+ return None
918
+
919
+ try:
920
+ while True:
921
+ if time.time() - start > MAX_DURATION:
922
+ logs.append("Stopped due to timeout.")
923
+ break
924
+
925
+ if pages_fetched >= MAX_PAGES:
926
+ logs.append("Stopped due to max pages.")
927
+ break
928
+
929
+ recent_activity_rate_limiter.wait()
930
+
931
+ params = {
932
+ "limit": limit,
933
+ "activityType": "discussion",
934
+ "feedType": "user",
935
+ "entity": username,
936
+ }
937
+ if cursor:
938
+ params["cursor"] = cursor
939
+ else:
940
+ params["skip"] = skip
941
+
942
+ response = session.get(
943
+ "https://huggingface.co/api/recent-activity",
944
+ params=params,
945
+ timeout=20,
946
+ )
947
+ response.raise_for_status()
948
+
949
+ payload = response.json()
950
+ pages_fetched += 1
951
+
952
+ events, next_cursor = normalize_payload(payload)
953
+
954
+ if not events:
955
+ logs.append("No more events.")
956
+ break
957
+
958
+ stop_early = False
959
+
960
+ for event in events:
961
+ if not isinstance(event, dict):
962
+ continue
963
+
964
+ event_ts = parse_ts(event.get("time"))
965
+ if event_ts is not None and event_ts.year < selected_year:
966
+ stop_early = True
967
+ break
968
+
969
+ discussion = event.get("discussionData") or {}
970
+ if not isinstance(discussion, dict):
971
+ continue
972
+
973
+ keep, unique_id = event_matches_mode(event, discussion)
974
+ if not keep or not unique_id:
975
+ continue
976
+
977
+ dt = extract_date_for_mode(event, discussion)
978
+ if dt is None or dt.year != selected_year:
979
+ continue
980
+
981
+ if unique_id in seen_ids:
982
+ continue
983
+
984
+ seen_ids.add(unique_id)
985
+ dates.append(dt)
986
+
987
+ if stop_early:
988
+ logs.append("Stopped early after reaching feed events older than selected year.")
989
+ break
990
+
991
+ if next_cursor:
992
+ cursor = next_cursor
993
+ else:
994
+ skip += limit
995
 
996
+ logs.append(f"Fetched {pages_fetched} page(s) from recent-activity.")
997
+ logs.append(f"Found {len(dates)} unique {mode} event(s) in {selected_year}.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
998
 
999
+ return {
1000
+ "title": title,
1001
+ "fetched": True,
1002
+ "dates": dates,
1003
+ "count": len(dates),
1004
+ "logs": "\n".join(logs),
1005
+ }
1006
 
1007
+ except Exception as e:
1008
+ return {
1009
+ "title": title,
1010
+ "fetched": True,
1011
+ "dates": [],
1012
+ "count": 0,
1013
+ "logs": f"{title} fetch failed: {str(e)}",
1014
+ }
1015
+
1016
+
1017
+ def fetch_all_community_activity(username: str, selected_year: int):
1018
+ username = (username or "").strip()
1019
+ selected_year = int(selected_year)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1020
 
1021
+ cache_key = (username.lower(), selected_year, "community")
1022
+ cached = get_cache(cache_key)
1023
+ if cached:
1024
+ return cached
1025
 
1026
+ if not username:
1027
+ state = empty_community_state(selected_year)
1028
+ state["fetched"] = True
1029
+ state["logs"] = "No username provided."
1030
+ set_cache(cache_key, state)
1031
+ return state
1032
 
1033
+ pr_opened = fetch_user_discussion_activity(username, selected_year, "pr_opened")
1034
+ pr_activity = fetch_user_discussion_activity(username, selected_year, "pr_activity")
1035
+ thread_opened = fetch_user_discussion_activity(username, selected_year, "thread_opened")
1036
+ discussion_activity = fetch_user_discussion_activity(username, selected_year, "discussion_activity")
1037
 
1038
+ combined_logs = "\n\n".join(
1039
+ [
1040
+ "[Pull Requests]\n" + pr_opened["logs"],
1041
+ "[PR Activity]\n" + pr_activity["logs"],
1042
+ "[Community Threads]\n" + thread_opened["logs"],
1043
+ "[Community Activity]\n" + discussion_activity["logs"],
1044
+ ]
1045
+ )
1046
 
1047
+ result = {
1048
+ "username": username,
1049
+ "selected_year": selected_year,
1050
+ "fetched": True,
1051
+ "pr_opened": pr_opened,
1052
+ "pr_activity": pr_activity,
1053
+ "thread_opened": thread_opened,
1054
+ "discussion_activity": discussion_activity,
1055
+ "logs": combined_logs,
1056
+ }
1057
 
1058
+ set_cache(cache_key, result)
1059
+ return result
 
1060
 
1061
 
1062
  # ============================================================
 
1319
  return render_status_badge(" · ".join(parts))
1320
 
1321
 
1322
+ def render_bucketed_idle_banner(
1323
+ section_key: str,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1324
  username,
1325
  selected_year,
1326
  profile: gr.OAuthProfile | None,
1327
  ):
1328
+ cfg = get_bucketed_cfg(section_key)
1329
  username = (username or "").strip()
1330
  selected_year = int(selected_year)
1331
  viewer = getattr(profile, "username", None) if profile else None
 
1337
  parts.append("Not logged in")
1338
 
1339
  if not username:
1340
+ parts.append(f"Enter a username to fetch {cfg['context_subject_label']}")
1341
  return render_status_badge(" · ".join(parts))
1342
 
1343
  if viewer and username.lower() == viewer.lower():
1344
+ parts.append(f"Fetch your {cfg['context_subject_label']} or enter another username")
1345
  parts.append(str(selected_year))
1346
  else:
1347
+ parts.append(f"Ready to fetch {cfg['context_subject_label']} for <b>{html.escape(username)}</b>")
1348
  parts.append(str(selected_year))
1349
 
1350
  return render_status_badge(" · ".join(parts))
1351
 
1352
 
1353
+ def render_bucketed_invalid_banner(section_key: str, username: str, error_text: str | None = None):
1354
+ _ = section_key
1355
  username = (username or "").strip()
1356
  message = error_text or f"Username or org '{username}' was not found."
1357
  return render_status_badge(html.escape(message))
1358
 
1359
 
1360
+ def render_bucketed_fetching_banner(
1361
+ section_key: str,
1362
  username,
1363
  selected_year,
1364
  profile: gr.OAuthProfile | None,
1365
  ):
1366
+ cfg = get_bucketed_cfg(section_key)
1367
  username = (username or "").strip()
1368
  selected_year = int(selected_year)
1369
  viewer = getattr(profile, "username", None) if profile else None
1370
 
1371
  if not username:
1372
+ return render_bucketed_idle_banner(section_key, "", selected_year, profile)
1373
 
1374
  parts = []
1375
  if viewer:
 
1378
  parts.append("Not logged in")
1379
 
1380
  if viewer and username.lower() == viewer.lower():
1381
+ parts.append(f"Fetching <b>your {cfg['context_subject_label']}</b>")
1382
  else:
1383
+ parts.append(f"Fetching {cfg['context_subject_label']} for <b>{html.escape(username)}</b>")
1384
 
1385
  parts.append(str(selected_year))
1386
  return render_status_badge(" · ".join(parts))
1387
 
1388
 
1389
+ def render_bucketed_loaded_banner(
1390
+ section_key: str,
1391
+ section_state: dict | None,
1392
  profile: gr.OAuthProfile | None,
1393
  ):
1394
+ cfg = get_bucketed_cfg(section_key)
1395
+ if not section_state or not section_state.get("username"):
1396
+ year = section_state.get("selected_year", datetime.now().year) if section_state else datetime.now().year
1397
+ return render_bucketed_idle_banner(section_key, "", year, profile)
1398
 
1399
+ username = section_state["username"]
1400
+ selected_year = int(section_state["selected_year"])
1401
  viewer = getattr(profile, "username", None) if profile else None
1402
 
1403
  parts = []
 
1407
  parts.append("Not logged in")
1408
 
1409
  if viewer and username.lower() == viewer.lower():
1410
+ parts.append(f"Viewing <b>your {cfg['context_subject_label']}</b>")
1411
  else:
1412
+ parts.append(f"Viewing {cfg['context_subject_label']} for <b>{html.escape(username)}</b>")
1413
 
1414
  parts.append(str(selected_year))
1415
  return render_status_badge(" · ".join(parts))
 
1567
  "opened": f"{len(opened)} PRs opened by {username} in {year}",
1568
  "activity": f"{len(activity)} PR follow-up events for {username} in {year}",
1569
  "both": f"{len(opened) + len(activity)} total PR events for {username} in {year}",
1570
+ }[mode]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1571
 
1572
  return render_contrib_chart(
1573
  df,
1574
  year,
1575
+ "Pull Requests",
1576
  palette_name="purple",
1577
  subtitle=subtitle,
1578
  )
1579
 
1580
 
1581
+ def render_discussion_combined_chart(community_state: dict | None, mode: str, username: str, year: int):
1582
+ if not community_state or not community_state.get("fetched"):
1583
+ return render_empty_chart("Community Discussions")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1584
 
1585
+ opened = community_state["thread_opened"]["dates"]
1586
+ activity = community_state["discussion_activity"]["dates"]
1587
+ merged = merge_dates(opened, activity, mode)
1588
  df = pd.DataFrame(merged, columns=["date"])
1589
 
1590
+ subtitle = {
1591
+ "opened": f"{len(opened)} threads opened by {username} in {year}",
1592
+ "activity": f"{len(activity)} discussion follow-up events for {username} in {year}",
1593
+ "both": f"{len(opened) + len(activity)} total discussion events for {username} in {year}",
1594
+ }[mode]
1595
+
1596
  return render_contrib_chart(
1597
  df,
1598
  year,
1599
+ "Community Discussions",
1600
+ palette_name="teal",
1601
  subtitle=subtitle,
1602
  )
1603
 
1604
 
1605
+ def render_bucketed_chart(section_key: str, section_state: dict | None, mode: str, username: str, year: int):
1606
+ cfg = get_bucketed_cfg(section_key)
 
1607
 
1608
+ if not section_state or not section_state.get("fetched"):
1609
+ return render_empty_chart(cfg["chart_title"])
1610
 
1611
+ bucket_titles = cfg["bucket_titles"]
1612
+ default_mode = cfg["default_mode"]
1613
+ mode = mode or default_mode
1614
+
1615
+ if mode == "all":
1616
+ merged = []
1617
+ for bucket_key in bucket_titles:
1618
+ merged.extend(section_state[bucket_key]["dates"])
1619
+ subtitle = f"{len(merged)} total {cfg['chart_title'].lower()} event(s) by {username} in {year}"
1620
  else:
1621
+ merged = list(section_state.get(mode, {}).get("dates", []))
1622
+ bucket_title = bucket_titles.get(mode, mode)
1623
+ subtitle = f"{len(merged)} {bucket_title.lower()} by {username} in {year}"
1624
 
1625
  df = pd.DataFrame(merged, columns=["date"])
1626
 
1627
  return render_contrib_chart(
1628
  df,
1629
  year,
1630
+ cfg["chart_title"],
1631
+ palette_name=cfg["palette_name"],
1632
  subtitle=subtitle,
1633
  )
1634
 
 
2062
  )
2063
 
2064
 
2065
+ def prepare_bucketed_fetch_ui(section_key: str, username, selected_year, profile: gr.OAuthProfile | None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2066
  username = (username or "").strip()
2067
+ if username:
2068
+ identity = validate_username(username, profile)
2069
+ if not identity.get("exists"):
2070
+ return render_bucketed_invalid_banner(section_key, username, identity.get("error"))
2071
+ return render_bucketed_fetching_banner(section_key, username, selected_year, profile)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2072
 
2073
 
2074
+ def maybe_reset_bucketed_section(
2075
+ section_key: str,
2076
+ current_state,
2077
  username,
2078
  selected_year,
2079
+ mode,
2080
  profile: gr.OAuthProfile | None,
2081
  ):
2082
+ cfg = get_bucketed_cfg(section_key)
2083
  username = (username or "").strip()
2084
  selected_year = int(selected_year)
2085
+ mode = mode or cfg["default_mode"]
2086
 
2087
  identity = validate_username(username, profile) if username else empty_identity("", "No username provided.")
2088
  if username and not identity.get("exists"):
2089
+ state = empty_generic_section_state(section_key, selected_year)
2090
  state["username"] = username
2091
  state["selected_year"] = selected_year
2092
  state["logs"] = identity.get("error") or f"Username or org '{username}' was not found."
2093
  return (
2094
  state,
2095
+ render_bucketed_invalid_banner(section_key, username, identity.get("error")),
2096
+ gr.update(value=cfg["default_mode"], visible="hidden"),
2097
+ render_empty_chart(cfg["chart_title"]),
2098
  state["logs"],
2099
  )
2100
 
2101
+ if not current_state:
2102
+ state = empty_generic_section_state(section_key, selected_year)
2103
  return (
2104
  state,
2105
+ render_bucketed_idle_banner(section_key, username, selected_year, profile),
2106
+ gr.update(value=cfg["default_mode"], visible="hidden"),
2107
+ render_empty_chart(cfg["chart_title"]),
2108
+ cfg["empty_logs_text"],
2109
  )
2110
 
2111
+ previous_username = (current_state.get("username") or "").strip()
2112
+ previous_year = int(current_state.get("selected_year") or selected_year)
2113
 
2114
  same_username = previous_username.lower() == username.lower()
2115
  same_year = previous_year == selected_year
2116
 
2117
+ if same_username and same_year and current_state.get("fetched"):
2118
+ chart = render_bucketed_chart(
2119
+ section_key,
2120
+ current_state,
2121
+ mode,
2122
  username=username,
2123
  year=selected_year,
2124
  )
2125
  return (
2126
+ current_state,
2127
+ render_bucketed_loaded_banner(section_key, current_state, profile),
2128
+ gr.update(value=mode, visible=True),
2129
  chart,
2130
+ current_state.get("logs", f"{cfg['chart_title']} activity already fetched."),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2131
  )
2132
 
2133
+ state = empty_generic_section_state(section_key, selected_year)
 
 
 
 
 
 
 
 
2134
  return (
2135
  state,
2136
+ render_bucketed_idle_banner(section_key, username, selected_year, profile),
2137
+ gr.update(value=cfg["default_mode"], visible="hidden"),
2138
+ render_empty_chart(cfg["chart_title"]),
2139
+ f"{cfg['chart_title']} activity needs to be fetched for the current username/year.",
2140
  )
2141
 
2142
 
2143
+ def fetch_and_render_bucketed_section(
2144
+ section_key: str,
2145
  current_state,
2146
  username,
2147
  selected_year,
2148
+ mode,
2149
  profile: gr.OAuthProfile | None,
2150
  ):
2151
+ _ = current_state
2152
+ cfg = get_bucketed_cfg(section_key)
2153
  username = (username or "").strip()
2154
  selected_year = int(selected_year)
2155
+ mode = mode or cfg["default_mode"]
2156
 
2157
  identity = validate_username(username, profile)
2158
  if username and not identity.get("exists"):
2159
+ empty_state = empty_generic_section_state(section_key, selected_year)
2160
  empty_state["username"] = username
2161
  empty_state["selected_year"] = selected_year
2162
  empty_state["logs"] = identity.get("error") or f"Username or org '{username}' was not found."
2163
  return (
2164
  empty_state,
2165
+ render_bucketed_invalid_banner(section_key, username, identity.get("error")),
2166
+ render_empty_chart(cfg["chart_title"]),
2167
  empty_state["logs"],
2168
+ gr.update(value=cfg["default_mode"], visible="hidden"),
2169
  )
2170
 
2171
+ state = fetch_generic_bucketed_section(section_key, username, selected_year)
2172
 
2173
+ chart = render_bucketed_chart(
2174
+ section_key,
2175
  state,
2176
+ mode,
2177
  username=username,
2178
  year=selected_year,
2179
  )
2180
 
2181
  return (
2182
  state,
2183
+ render_bucketed_loaded_banner(section_key, state, profile),
2184
  chart,
2185
  state["logs"],
2186
+ gr.update(value=mode, visible=True),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2187
  )
2188
 
2189
 
2190
+ def rerender_bucketed_from_state(section_key: str, section_state, mode):
2191
+ cfg = get_bucketed_cfg(section_key)
2192
+ if not section_state:
2193
+ return render_empty_chart(cfg["chart_title"])
2194
+ return render_bucketed_chart(
2195
+ section_key,
2196
+ section_state,
2197
+ mode or cfg["default_mode"],
2198
+ username=(section_state.get("username") or ""),
2199
+ year=int(section_state.get("selected_year") or datetime.now().year),
2200
  )
2201
 
2202
 
 
2218
 
2219
  repo_context_html = render_idle_context_banner(effective_username, profile)
2220
  community_context_html = render_community_idle_banner(effective_username, selected_year, profile)
2221
+ upvotes_context_html = render_bucketed_idle_banner("upvotes", effective_username, selected_year, profile)
2222
+ likes_context_html = render_bucketed_idle_banner("likes", effective_username, selected_year, profile)
2223
+ papers_context_html = render_bucketed_idle_banner("papers", effective_username, selected_year, profile)
2224
 
2225
  return (
2226
  username_update,
 
2885
 
2886
  with gr.Column(elem_classes="toggles-container"):
2887
  upvotes_view_mode = gr.Radio(
2888
+ choices=[(label, key) for key, label in BUCKETED_SECTION_CONFIGS["upvotes"]["mode_labels"].items()],
2889
+ value=BUCKETED_SECTION_CONFIGS["upvotes"]["default_mode"],
2890
+ label=BUCKETED_SECTION_CONFIGS["upvotes"]["radio_label"],
 
 
 
 
 
2891
  visible="hidden",
2892
  elem_classes=["gh-toggle-radio"],
2893
  elem_id="upvotes-view-toggle",
2894
  )
2895
 
2896
+ upvotes_chart = gr.HTML(render_empty_chart(BUCKETED_SECTION_CONFIGS["upvotes"]["chart_title"]))
2897
 
2898
  upvotes_logs_box = gr.Textbox(
2899
  label="Upvotes Fetch Logs",
2900
  interactive=False,
2901
  lines=10,
2902
+ value=BUCKETED_SECTION_CONFIGS["upvotes"]["empty_logs_text"],
2903
  )
2904
 
2905
  gr.Markdown("## Likes activity")
 
2911
 
2912
  with gr.Column(elem_classes="toggles-container"):
2913
  likes_view_mode = gr.Radio(
2914
+ choices=[(label, key) for key, label in BUCKETED_SECTION_CONFIGS["likes"]["mode_labels"].items()],
2915
+ value=BUCKETED_SECTION_CONFIGS["likes"]["default_mode"],
2916
+ label=BUCKETED_SECTION_CONFIGS["likes"]["radio_label"],
 
 
 
 
 
2917
  visible="hidden",
2918
  elem_classes=["gh-toggle-radio"],
2919
  elem_id="likes-view-toggle",
2920
  )
2921
 
2922
+ likes_chart = gr.HTML(render_empty_chart(BUCKETED_SECTION_CONFIGS["likes"]["chart_title"]))
2923
 
2924
  likes_logs_box = gr.Textbox(
2925
  label="Likes Fetch Logs",
2926
  interactive=False,
2927
  lines=10,
2928
+ value=BUCKETED_SECTION_CONFIGS["likes"]["empty_logs_text"],
2929
  )
2930
 
2931
  gr.Markdown("## Papers activity")
 
2937
 
2938
  with gr.Column(elem_classes="toggles-container"):
2939
  papers_view_mode = gr.Radio(
2940
+ choices=[(label, key) for key, label in BUCKETED_SECTION_CONFIGS["papers"]["mode_labels"].items()],
2941
+ value=BUCKETED_SECTION_CONFIGS["papers"]["default_mode"],
2942
+ label=BUCKETED_SECTION_CONFIGS["papers"]["radio_label"],
 
 
 
 
2943
  visible="hidden",
2944
  elem_classes=["gh-toggle-radio"],
2945
  elem_id="papers-view-toggle",
2946
  )
2947
 
2948
+ papers_chart = gr.HTML(render_empty_chart(BUCKETED_SECTION_CONFIGS["papers"]["chart_title"]))
2949
 
2950
  papers_logs_box = gr.Textbox(
2951
  label="Papers Fetch Logs",
2952
  interactive=False,
2953
  lines=10,
2954
+ value=BUCKETED_SECTION_CONFIGS["papers"]["empty_logs_text"],
2955
  )
2956
 
2957
  repo_fetch_outputs = [
 
3049
  demo.load(
3050
  fn=hydrate_header_on_load,
3051
  inputs=[username, selected_year],
3052
+ outputs=[
3053
+ username,
3054
+ context_box,
3055
+ community_context_box,
3056
+ upvotes_context_box,
3057
+ likes_context_box,
3058
+ papers_context_box,
3059
+ ],
3060
  show_progress="hidden",
3061
  )
3062
 
 
3084
  outputs=community_reset_outputs,
3085
  show_progress="hidden",
3086
  ).then(
3087
+ fn=lambda state, uname, year, mode, profile: maybe_reset_bucketed_section("upvotes", state, uname, year, mode, profile),
3088
  inputs=[upvotes_state, username, selected_year, upvotes_view_mode],
3089
  outputs=upvotes_reset_outputs,
3090
  show_progress="hidden",
3091
  ).then(
3092
+ fn=lambda state, uname, year, mode, profile: maybe_reset_bucketed_section("likes", state, uname, year, mode, profile),
3093
  inputs=[likes_state, username, selected_year, likes_view_mode],
3094
  outputs=likes_reset_outputs,
3095
  show_progress="hidden",
3096
  ).then(
3097
+ fn=lambda state, uname, year, mode, profile: maybe_reset_bucketed_section("papers", state, uname, year, mode, profile),
3098
  inputs=[papers_state, username, selected_year, papers_view_mode],
3099
  outputs=papers_reset_outputs,
3100
  show_progress="hidden",
 
3139
  )
3140
 
3141
  fetch_upvotes_button.click(
3142
+ fn=lambda uname, year, profile: prepare_bucketed_fetch_ui("upvotes", uname, year, profile),
3143
  inputs=[username, selected_year],
3144
  outputs=upvotes_context_box,
3145
  show_progress="hidden",
3146
  ).then(
3147
+ fn=lambda state, uname, year, mode, profile: fetch_and_render_bucketed_section("upvotes", state, uname, year, mode, profile),
3148
  inputs=[upvotes_state, username, selected_year, upvotes_view_mode],
3149
  outputs=upvotes_fetch_outputs,
3150
  show_progress="full",
 
3155
  )
3156
 
3157
  upvotes_view_mode.change(
3158
+ fn=lambda state, mode: rerender_bucketed_from_state("upvotes", state, mode),
3159
  inputs=[upvotes_state, upvotes_view_mode],
3160
  outputs=upvotes_chart,
3161
  show_progress="hidden",
3162
  )
3163
 
3164
  fetch_likes_button.click(
3165
+ fn=lambda uname, year, profile: prepare_bucketed_fetch_ui("likes", uname, year, profile),
3166
  inputs=[username, selected_year],
3167
  outputs=likes_context_box,
3168
  show_progress="hidden",
3169
  ).then(
3170
+ fn=lambda state, uname, year, mode, profile: fetch_and_render_bucketed_section("likes", state, uname, year, mode, profile),
3171
  inputs=[likes_state, username, selected_year, likes_view_mode],
3172
  outputs=likes_fetch_outputs,
3173
  show_progress="full",
 
3178
  )
3179
 
3180
  likes_view_mode.change(
3181
+ fn=lambda state, mode: rerender_bucketed_from_state("likes", state, mode),
3182
  inputs=[likes_state, likes_view_mode],
3183
  outputs=likes_chart,
3184
  show_progress="hidden",
3185
  )
3186
 
3187
  fetch_papers_button.click(
3188
+ fn=lambda uname, year, profile: prepare_bucketed_fetch_ui("papers", uname, year, profile),
3189
  inputs=[username, selected_year],
3190
  outputs=papers_context_box,
3191
  show_progress="hidden",
3192
  ).then(
3193
+ fn=lambda state, uname, year, mode, profile: fetch_and_render_bucketed_section("papers", state, uname, year, mode, profile),
3194
  inputs=[papers_state, username, selected_year, papers_view_mode],
3195
  outputs=papers_fetch_outputs,
3196
  show_progress="full",
 
3201
  )
3202
 
3203
  papers_view_mode.change(
3204
+ fn=lambda state, mode: rerender_bucketed_from_state("papers", state, mode),
3205
  inputs=[papers_state, papers_view_mode],
3206
  outputs=papers_chart,
3207
  show_progress="hidden",