The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .github
    ├── copilot-instructions.md
    └── workflows
    │   └── pr‑stats.yml
├── .gitignore
├── README.md
├── collect_data.py
├── data.csv
├── data_backup.csv
├── docs
    ├── CNAME
    ├── _config.yml
    ├── chart-data.json
    ├── chart.png
    ├── index.html
    └── styles.css
├── generate_chart.py
├── requirements.txt
├── scripts
    ├── add_nondraft_final.py
    └── reconcile_codegen_merged.py
└── templates
    ├── index_template.html
    ├── index_template_old.html
    ├── index_template_simple.html
    └── readme_template.md


/.github/copilot-instructions.md:
--------------------------------------------------------------------------------
1 | always use our venv called venv
2 | 
3 | main way to run the end to end workflow:
4 | python3 collect_data.py # this gets new data into data.csv
5 | python3 generate_chart.py # this generates the chart image, renders the index template, the readme template.
6 | python3 -m http.server 8000 # this runs our web server so we can see the index page.
7 | 


--------------------------------------------------------------------------------
/.github/workflows/pr‑stats.yml:
--------------------------------------------------------------------------------
 1 | name: PR stats
 2 | 
 3 | on:
 4 |   schedule:
 5 |     - cron: "0 */3 * * *"
 6 |   workflow_dispatch:
 7 | 
 8 | permissions:
 9 |   contents: write
10 | 
11 | jobs:
12 |   track:
13 |     runs-on: ubuntu-latest
14 |     steps:
15 |       - uses: actions/checkout@v4
16 | 
17 |       - uses: actions/setup-python@v5
18 |         with: { python-version: "3.x" }
19 | 
20 |       - run: pip install --quiet -r requirements.txt
21 | 
22 |       - name: Collect PR data
23 |         run: python collect_data.py
24 |         env:
25 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26 | 
27 |       - name: Generate chart
28 |         run: python generate_chart.py
29 | 
30 |       - name: Commit chart
31 |         uses: stefanzweifel/git-auto-commit-action@v5
32 |         with:
33 |           commit_message: "chore: update PR‑approval chart"
34 |           file_pattern: "data.csv README.md docs/index.html docs/chart.png docs/chart-data.json"
35 |           commit_author: "github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
36 |           commit_user_name: "github-actions[bot]"
37 |           commit_user_email: "github-actions[bot]@users.noreply.github.com"
38 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | # Byte-compiled / optimized / DLL files
 2 | __pycache__/
 3 | *.py[cod]
 4 | *$py.class
 5 | 
 6 | # Distribution / packaging
 7 | .Python
 8 | build/
 9 | develop-eggs/
10 | dist/
11 | downloads/
12 | eggs/
13 | .eggs/
14 | lib/
15 | lib64/
16 | parts/
17 | sdist/
18 | var/
19 | wheels/
20 | pip-wheel-metadata/
21 | share/python-wheels/
22 | *.egg-info/
23 | .installed.cfg
24 | *.egg
25 | MANIFEST
26 | 
27 | # PyInstaller
28 | #  Usually these files are written by a python script from a template
29 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 | 
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 | 
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .nox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | *.py,cover
48 | .hypothesis/
49 | .pytest_cache/
50 | 
51 | # Virtual environments
52 | .env
53 | .venv
54 | env/
55 | venv/
56 | ENV/
57 | env.bak/
58 | venv.bak/
59 | 
60 | # IDEs
61 | .vscode/
62 | .idea/
63 | *.swp
64 | *.swo
65 | *~
66 | 
67 | # OS
68 | .DS_Store
69 | .DS_Store?
70 | ._*
71 | .Spotlight-V100
72 | .Trashes
73 | ehthumbs.db
74 | Thumbs.dbdata_backup.csv
75 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | ### PR Analytics: Volume vs Success Rate (auto-updated)
 2 | 
 3 | View the [interactive dashboard](https://prarena.ai) for these statistics.
 4 | 
 5 | ## Understanding the Metrics
 6 | 
 7 | Different AI coding agents follow different workflows when creating pull requests:
 8 | 
 9 | - **All PRs**: Every pull request created by an agent, including DRAFT PRs
10 | - **Ready PRs**: Non-draft pull requests that are ready for review and merging
11 | - **Merged PRs**: Pull requests that were successfully merged into the codebase
12 | 
13 | **Key workflow differences**: Some agents like **Codex** iterate privately and create ready PRs directly, resulting in very few drafts but high merge rates. Others like **Copilot** and **Codegen** create draft PRs first, encouraging public iteration before marking them ready for review.
14 | 
15 | The statistics below focus on **Ready PRs only** to fairly compare agents across different workflows, measuring each agent's ability to produce mergeable code regardless of whether they iterate publicly (with drafts) or privately.
16 | 
17 | ## Data sources
18 | 
19 | Explore the GitHub search queries used:
20 | 
21 | 
22 | 
23 | - **All Copilot PRs**: [https://github.com/search?q=is:pr+head:copilot/&type=pullrequests](https://github.com/search?q=is:pr+head:copilot/&type=pullrequests)
24 | - **Merged Copilot PRs**: [https://github.com/search?q=is:pr+head:copilot/+is:merged&type=pullrequests](https://github.com/search?q=is:pr+head:copilot/+is:merged&type=pullrequests)
25 |   
26 | 
27 | - **All Codex PRs**: [https://github.com/search?q=is:pr+head:codex/&type=pullrequests](https://github.com/search?q=is:pr+head:codex/&type=pullrequests)
28 | - **Merged Codex PRs**: [https://github.com/search?q=is:pr+head:codex/+is:merged&type=pullrequests](https://github.com/search?q=is:pr+head:codex/+is:merged&type=pullrequests)
29 |   
30 | 
31 | - **All Cursor PRs**: [https://github.com/search?q=is:pr+head:cursor/&type=pullrequests](https://github.com/search?q=is:pr+head:cursor/&type=pullrequests)
32 | - **Merged Cursor PRs**: [https://github.com/search?q=is:pr+head:cursor/+is:merged&type=pullrequests](https://github.com/search?q=is:pr+head:cursor/+is:merged&type=pullrequests)
33 |   
34 | 
35 | - **All Devin PRs**: [https://github.com/search?q=is:pr+author:devin-ai-integration[bot]&type=pullrequests](https://github.com/search?q=is:pr+author:devin-ai-integration[bot]&type=pullrequests)
36 | - **Merged Devin PRs**: [https://github.com/search?q=is:pr+author:devin-ai-integration[bot]+is:merged&type=pullrequests](https://github.com/search?q=is:pr+author:devin-ai-integration[bot]+is:merged&type=pullrequests)
37 |   
38 | 
39 | - **All Codegen PRs**: [https://github.com/search?q=is:pr+author:codegen-sh[bot]&type=pullrequests](https://github.com/search?q=is:pr+author:codegen-sh[bot]&type=pullrequests)
40 | - **Merged Codegen PRs**: [https://github.com/search?q=is:pr+author:codegen-sh[bot]+is:merged&type=pullrequests](https://github.com/search?q=is:pr+author:codegen-sh[bot]+is:merged&type=pullrequests)
41 |   
42 | 
43 | ---
44 | 
45 | ![chart](docs/chart.png)
46 | 
47 | ## Current Statistics
48 | 
49 | | Project | Ready PRs | Merged PRs | Success Rate |
50 | | ------- | --------- | ---------- | ------------ |
51 | | Copilot | 24,213 | 22,200 | 91.69% |
52 | | Codex | 711,271 | 625,691 | 87.97% |
53 | | Cursor | 18,954 | 14,436 | 76.16% |
54 | | Devin | 30,253 | 19,908 | 65.81% |
55 | | Codegen | 2,395 | 1,847 | 77.12% |


--------------------------------------------------------------------------------
/collect_data.py:
--------------------------------------------------------------------------------
  1 | import csv
  2 | import datetime as dt
  3 | import os
  4 | import re
  5 | from pathlib import Path
  6 | import requests
  7 | import time
  8 | 
  9 | 
 10 | # GitHub API headers with optional authentication
 11 | def get_headers():
 12 |     headers = {"Accept": "application/vnd.github+json", "User-Agent": "PR-Watcher"}
 13 | 
 14 |     # Add authentication if token is available
 15 |     github_token = os.getenv("GITHUB_TOKEN")
 16 |     if github_token:
 17 |         headers["Authorization"] = f"Bearer {github_token}"
 18 |         print("Using authenticated GitHub API requests")
 19 |     else:
 20 |         print("Using unauthenticated GitHub API requests (rate limited)")
 21 | 
 22 |     return headers
 23 | 
 24 | 
 25 | # Search queries - tracking all PR metrics
 26 | # Organized by agent: total, merged, non-draft for each
 27 | Q = {
 28 |     # Copilot metrics
 29 |     "is:pr+head:copilot/": "copilot_total",
 30 |     "is:pr+head:copilot/+is:merged": "copilot_merged",
 31 |     "is:pr+head:copilot/+-is:draft": "copilot_nondraft",
 32 |     # Codex metrics
 33 |     "is:pr+head:codex/": "codex_total",
 34 |     "is:pr+head:codex/+is:merged": "codex_merged",
 35 |     "is:pr+head:codex/+-is:draft": "codex_nondraft",
 36 |     # Cursor metrics
 37 |     "is:pr+head:cursor/": "cursor_total",
 38 |     "is:pr+head:cursor/+is:merged": "cursor_merged",
 39 |     "is:pr+head:cursor/+-is:draft": "cursor_nondraft",
 40 |     # Devin metrics
 41 |     "is:pr+author:devin-ai-integration[bot]": "devin_total",
 42 |     "is:pr+author:devin-ai-integration[bot]+is:merged": "devin_merged",
 43 |     "is:pr+author:devin-ai-integration[bot]+-is:draft": "devin_nondraft",
 44 |     # Codegen metrics
 45 |     "is:pr+author:codegen-sh[bot]": "codegen_total",
 46 |     "is:pr+author:codegen-sh[bot]+is:merged": "codegen_merged",
 47 |     "is:pr+author:codegen-sh[bot]+-is:draft": "codegen_nondraft",
 48 | }
 49 | 
 50 | 
 51 | def collect_data():
 52 |     # Get data from GitHub API - 15 metrics total (3 per agent: total, merged, non-draft)
 53 |     cnt = {}
 54 | 
 55 |     # Get headers with authentication if available
 56 |     headers = get_headers()
 57 | 
 58 |     # Collect all metrics in one loop
 59 |     for query, key in Q.items():
 60 |         print(f"Collecting {key}...")
 61 |         r = requests.get(
 62 |             f"https://api.github.com/search/issues?q={query}",
 63 |             headers=headers,
 64 |             timeout=30,
 65 |         )
 66 |         r.raise_for_status()
 67 |         cnt[key] = r.json()["total_count"]
 68 |         print(f"  {key}: {cnt[key]}")
 69 | 
 70 |         # Rate limiting: wait half a second between API calls
 71 |         time.sleep(0.5)
 72 | 
 73 |     # Save data to CSV
 74 |     timestamp = dt.datetime.now(dt.UTC).strftime("%Y‑%m‑%d %H:%M:%S")
 75 |     row = [
 76 |         timestamp,
 77 |         cnt["copilot_total"],
 78 |         cnt["copilot_merged"],
 79 |         cnt["codex_total"],
 80 |         cnt["codex_merged"],
 81 |         cnt["cursor_total"],
 82 |         cnt["cursor_merged"],
 83 |         cnt["devin_total"],
 84 |         cnt["devin_merged"],
 85 |         cnt["codegen_total"],
 86 |         cnt["codegen_merged"],
 87 |         cnt["copilot_nondraft"],
 88 |         cnt["codex_nondraft"],
 89 |         cnt["cursor_nondraft"],
 90 |         cnt["devin_nondraft"],
 91 |         cnt["codegen_nondraft"],
 92 |     ]
 93 | 
 94 |     csv_file = Path("data.csv")
 95 |     is_new_file = not csv_file.exists()
 96 |     with csv_file.open("a", newline="") as f:
 97 |         writer = csv.writer(f)
 98 |         if is_new_file:
 99 |             writer.writerow(
100 |                 [
101 |                     "timestamp",
102 |                     "copilot_total",
103 |                     "copilot_merged",
104 |                     "codex_total",
105 |                     "codex_merged",
106 |                     "cursor_total",
107 |                     "cursor_merged",
108 |                     "devin_total",
109 |                     "devin_merged",
110 |                     "codegen_total",
111 |                     "codegen_merged",
112 |                     "copilot_nondraft",
113 |                     "codex_nondraft",
114 |                     "cursor_nondraft",
115 |                     "devin_nondraft",
116 |                     "codegen_nondraft",
117 |                 ]
118 |             )
119 |         writer.writerow(row)
120 | 
121 |     return csv_file
122 | 
123 | 
124 | def update_html_with_latest_data():
125 |     """Update the HTML file with the latest statistics from the chart data."""
126 |     # The HTML will be updated by JavaScript automatically when chart-data.json loads
127 |     # This is a placeholder for any additional HTML updates needed
128 |     html_file = Path("docs/index.html")
129 |     if not html_file.exists():
130 |         print("HTML file not found, skipping HTML update")
131 |         return
132 | 
133 |     # Update the last updated timestamp in the HTML
134 |     html_content = html_file.read_text()
135 | 
136 |     # Get current timestamp in the format used in the HTML
137 |     now = dt.datetime.now(dt.UTC)
138 |     timestamp_str = now.strftime("%B %d, %Y %H:%M UTC")
139 | 
140 |     # Update the timestamp in the HTML
141 |     updated_html = re.sub(
142 |         r'<span id="last-updated">[^<]*</span>',
143 |         f'<span id="last-updated">{timestamp_str}</span>',
144 |         html_content,
145 |     )
146 | 
147 |     html_file.write_text(updated_html)
148 |     print(f"Updated HTML timestamp to: {timestamp_str}")
149 | 
150 | 
151 | if __name__ == "__main__":
152 |     collect_data()
153 |     update_html_with_latest_data()
154 |     print("Data collection complete. To generate chart, run generate_chart.py")
155 | 


--------------------------------------------------------------------------------
/data_backup.csv:
--------------------------------------------------------------------------------
  1 | timestamp,copilot_total,copilot_merged,codex_total,codex_merged,cursor_total,cursor_merged,devin_total,devin_merged,codegen_total,codegen_merged,copilot_nondraft,codex_nondraft,cursor_nondraft,devin_nondraft,codegen_nondraft
  2 | 2025‑05‑26 14:21:34,8729,1594,54211,44230,0,0,0,0,0,0,2095,51605,117,23433,1206
  3 | 2025‑05‑26 14:41:16,8772,1600,54285,44297,0,0,0,0,0,0,2100,51656,117,23435,1206
  4 | 2025‑05‑26 14:44:33,8773,1604,54300,44309,0,0,0,0,0,0,2100,51665,117,23436,1206
  5 | 2025‑05‑26 15:01:03,8777,1608,54361,44361,0,0,0,0,0,0,2104,51708,117,23438,1207
  6 | 2025‑05‑26 18:00:59,8817,1637,54720,44677,0,0,0,0,0,0,2145,52176,119,23459,1209
  7 | 2025‑05‑26 21:00:49,8852,1662,55314,45185,0,0,0,0,0,0,2187,52644,121,23481,1212
  8 | 2025‑05‑27 00:01:50,8883,1684,55799,45606,0,0,0,0,0,0,2228,53115,122,23503,1215
  9 | 2025‑05‑27 03:18:57,8953,1726,56247,45977,0,0,0,0,0,0,2274,53628,124,23527,1218
 10 | 2025‑05‑27 06:01:42,8992,1752,56660,46338,0,0,0,0,0,0,2311,54051,126,23546,1221
 11 | 2025‑05‑27 09:01:03,9057,1798,57288,46897,0,0,0,0,0,0,2352,54518,127,23568,1224
 12 | 2025‑05‑27 12:01:08,9101,1828,57822,47375,0,0,0,0,0,0,2394,54986,129,23590,1226
 13 | 2025‑05‑27 15:01:07,9215,1898,58093,47629,0,0,0,0,0,0,2435,55454,131,23611,1229
 14 | 2025‑05‑27 18:00:58,9276,1932,58600,48092,0,0,0,0,0,0,2476,55922,132,23633,1232
 15 | 2025‑05‑27 18:53:59,9291,1937,58807,48284,0,0,0,0,0,0,2489,56060,133,23639,1233
 16 | 2025‑05‑27 21:00:55,9329,1964,59220,48668,0,0,0,0,0,0,2518,56391,134,23655,1235
 17 | 2025‑05‑28 00:02:01,9418,2022,59533,48942,0,0,0,0,0,0,2559,56862,136,23677,1238
 18 | 2025‑05‑28 03:19:55,9429,2055,60227,49553,0,0,0,0,0,0,2605,57377,138,23700,1241
 19 | 2025‑05‑28 06:01:05,9459,2080,60579,49885,0,0,0,0,0,0,2642,57796,139,23720,1243
 20 | 2025‑05‑28 09:01:03,9485,2120,61112,50381,0,0,0,0,0,0,2683,58264,141,23742,1246
 21 | 2025‑05‑28 12:01:14,9520,2153,61706,50918,0,0,0,0,0,0,2725,58733,142,23763,1249
 22 | 2025‑05‑28 15:01:05,9571,2183,62379,51505,0,0,0,0,0,0,2766,59201,144,23785,1252
 23 | 2025‑05‑28 18:01:05,9628,2213,62951,52001,0,0,0,0,0,0,2801,59629,145,23807,1254
 24 | 2025‑05‑28 21:01:04,9672,2250,63934,52934,0,0,0,0,0,0,2836,60056,147,23829,1256
 25 | 2025‑05‑29 00:01:51,9740,2286,65005,53822,0,0,0,0,0,0,2871,60486,148,23851,1258
 26 | 2025‑05‑29 03:20:23,9778,2317,65748,54460,0,0,0,0,0,0,2910,60958,150,23875,1260
 27 | 2025‑05‑29 06:01:00,9826,2342,66209,54888,0,0,0,0,0,0,2941,61339,151,23894,1262
 28 | 2025‑05‑29 09:01:12,9801,2365,66582,55221,0,0,0,0,0,0,2976,61767,152,23916,1264
 29 | 2025‑05‑29 12:01:13,9801,2396,67084,55676,0,0,0,0,0,0,3011,62195,153,23938,1266
 30 | 2025‑05‑29 15:01:04,9886,2450,67676,56196,0,0,0,0,0,0,3046,62622,155,23960,1268
 31 | 2025‑05‑29 18:01:03,9946,2477,68280,56711,0,0,0,0,0,0,3081,63050,156,23982,1269
 32 | 2025‑05‑29 21:01:03,10021,2528,68849,57244,0,0,0,0,0,0,3116,63478,157,24003,1271
 33 | 2025‑05‑30 00:02:06,10056,2556,69319,57646,0,0,0,0,0,0,3152,63908,159,24025,1273
 34 | 2025‑05‑30 03:19:21,10092,2593,69817,58062,0,0,0,0,0,0,3190,64376,160,24049,1275
 35 | 2025‑05‑30 06:00:55,10141,2626,70115,58282,0,0,0,0,0,0,3221,64760,161,24069,1277
 36 | 2025‑05‑30 09:00:57,10160,2651,70487,58626,0,0,0,0,0,0,3256,65188,163,24091,1279
 37 | 2025‑05‑30 12:01:08,10187,2662,70867,58899,0,0,0,0,0,0,3291,65616,164,24113,1281
 38 | 2025‑05‑30 15:01:00,10240,2694,71526,59448,0,0,0,0,0,0,3326,66044,165,24134,1283
 39 | 2025‑05‑30 18:01:07,10286,2733,72215,60080,0,0,0,0,0,0,3362,66471,167,24156,1285
 40 | 2025‑05‑30 21:00:52,10340,2788,72199,60014,0,0,0,0,0,0,3396,66899,168,24178,1287
 41 | 2025‑05‑31 00:02:00,10391,2824,70675,58469,0,0,0,0,0,0,3432,67329,170,24200,1289
 42 | 2025‑05‑31 03:18:35,10442,2859,70991,58737,0,0,0,0,0,0,3470,67796,171,24224,1291
 43 | 2025‑05‑31 06:01:07,10488,2897,71170,58918,0,0,0,0,0,0,3506,68146,173,24239,1294
 44 | 2025‑05‑31 09:00:56,10520,2911,71581,59248,0,0,0,0,0,0,3546,68532,174,24255,1297
 45 | 2025‑05‑31 12:01:13,10581,2948,71991,59588,0,0,0,0,0,0,3586,68920,176,24272,1300
 46 | 2025‑05‑31 15:00:53,10629,2996,72358,59910,0,0,0,0,0,0,3626,69307,178,24288,1303
 47 | 2025‑05‑31 18:00:59,10689,3037,72762,60274,0,0,0,0,0,0,3666,69694,179,24305,1306
 48 | 2025‑05‑31 21:00:57,10763,3089,73174,60634,0,0,0,0,0,0,3706,70081,181,24321,1308
 49 | 2025‑06‑01 00:02:07,10846,3140,72953,60522,0,0,0,0,0,0,3746,70471,183,24338,1311
 50 | 2025‑06‑01 03:29:15,10891,3171,73489,61021,0,0,0,0,0,0,3792,70916,185,24357,1315
 51 | 2025‑06‑01 06:01:02,10920,3196,73791,61287,0,0,0,0,0,0,3826,71243,186,24371,1317
 52 | 2025‑06‑01 09:01:03,10949,3215,74239,61672,0,0,0,0,0,0,3865,71630,188,24387,1320
 53 | 2025‑06‑01 12:01:08,10973,3255,74493,61902,0,0,0,0,0,0,3905,72018,190,24404,1323
 54 | 2025‑06‑01 15:01:00,11005,3295,74935,62304,0,0,0,0,0,0,3945,72404,192,24420,1326
 55 | 2025‑06‑01 18:01:08,11072,3350,75443,62780,0,0,0,0,0,0,3985,72792,193,24437,1329
 56 | 2025‑06‑01 18:59:52,11090,3371,75635,62962,0,0,0,0,0,0,3998,72918,194,24442,1330
 57 | 2025‑06‑01 19:04:17,11091,3372,75643,62973,0,0,27225,16620,0,0,3999,72928,194,24442,1330
 58 | 2025‑06‑01 21:00:52,11163,3402,76007,63286,0,0,27231,16623,0,0,4025,73179,195,24453,1332
 59 | 2025‑06‑02 00:01:50,11184,3445,76494,63710,0,0,27249,16633,0,0,4065,73568,197,24470,1335
 60 | 2025‑06‑02 03:24:55,11250,3513,76763,63923,0,0,27272,16653,0,0,4110,74005,199,24488,1338
 61 | 2025‑06‑02 06:01:05,11274,3534,77017,64142,0,0,27290,16671,0,0,4145,74341,200,24503,1341
 62 | 2025‑06‑02 09:01:10,11311,3569,77456,64517,0,0,27313,16690,0,0,4185,74728,202,24519,1344
 63 | 2025‑06‑02 12:01:04,11358,3592,77960,64949,0,0,27345,16719,0,0,4225,76506,204,24540,1348
 64 | 2025‑06‑02 15:01:02,11424,3641,78311,65225,0,0,27361,16728,0,0,4266,78284,205,24562,1352
 65 | 2025‑06‑02 18:01:01,11507,3716,78795,65647,0,0,27386,16746,0,0,4306,80063,207,24583,1356
 66 | 2025‑06‑02 21:00:59,11572,3757,79303,66160,0,0,27401,16761,0,0,4346,81841,209,24605,1360
 67 | 2025‑06‑03 00:01:57,11625,3794,79759,66568,0,0,27428,16774,0,0,4387,83630,210,24626,1364
 68 | 2025‑06‑03 03:21:22,11654,3831,80034,66795,0,0,27281,16612,0,0,4431,85600,212,24650,1369
 69 | 2025‑06‑03 06:01:08,11701,3863,80347,67053,0,0,27299,16624,0,0,4467,87179,214,24669,1372
 70 | 2025‑06‑03 09:01:16,11703,3891,80843,67495,0,0,27326,16638,0,0,4507,88959,215,24691,1376
 71 | 2025‑06‑03 12:01:12,11790,3944,81145,67753,0,0,27351,16652,0,0,4547,90737,217,24712,1380
 72 | 2025‑06‑03 15:01:12,11836,3970,81669,68203,0,0,27390,16691,0,0,4588,92516,219,24734,1385
 73 | 2025‑06‑03 18:01:08,11945,4015,82303,68785,0,0,27413,16704,0,0,4628,94294,221,24755,1389
 74 | 2025‑06‑03 21:01:09,12007,4056,85988,71508,0,0,27436,16718,0,0,4668,96073,222,24777,1393
 75 | 2025‑06‑04 00:02:07,12058,4088,89563,74245,0,0,27457,16735,0,0,4709,97862,224,24798,1397
 76 | 2025‑06‑04 03:21:45,12116,4122,93329,77062,0,0,27414,16685,0,0,4753,99834,226,24822,1401
 77 | 2025‑06‑04 06:01:08,12135,4141,95829,79045,0,0,27425,16696,0,0,4789,101409,227,24841,1405
 78 | 2025‑06‑04 09:01:04,12180,4178,99685,81911,0,0,27452,16715,0,0,4829,103187,229,24863,1409
 79 | 2025‑06‑04 12:01:14,12226,4205,103723,84974,0,0,27466,16726,0,0,4870,104968,231,24884,1413
 80 | 2025‑06‑04 15:11:29,12310,4251,108709,88950,0,0,27497,16752,0,0,4912,106848,232,24907,1417
 81 | 2025‑06‑04 17:12:37,12354,4276,111781,91560,244,174,27480,16736,0,0,4939,108045,234,24921,1420
 82 | 2025‑06‑04 18:00:59,12380,4280,113011,92601,245,175,27485,16737,0,0,4950,108523,234,24927,1421
 83 | 2025‑06‑04 21:01:13,12416,4311,117399,96208,245,175,27516,16753,0,0,4994,111555,246,24943,1424
 84 | 2025‑06‑05 00:02:03,12499,4345,120664,98859,251,179,27547,16766,0,0,5038,114598,257,24960,1427
 85 | 2025‑06‑05 03:21:45,12574,4389,124279,101930,261,189,27584,16790,0,0,5086,117958,270,24978,1430
 86 | 2025‑06‑05 06:01:06,12595,4405,126570,103797,276,200,27604,16810,0,0,5125,120639,280,24992,1433
 87 | 2025‑06‑05 09:01:17,12637,4427,129434,106061,296,214,27627,16825,0,0,5169,123671,292,25009,1436
 88 | 2025‑06‑05 12:01:13,12685,4450,132583,108609,325,243,27644,16845,0,0,5213,126698,303,25025,1439
 89 | 2025‑06‑05 15:01:08,12770,4510,136413,111692,332,245,27658,16856,0,0,5257,129725,315,25042,1442
 90 | 2025‑06‑05 19:34:27,12869,4567,142096,116376,343,254,27679,16869,0,0,5323,134324,332,25066,1446
 91 | 2025‑06‑05 19:56:50,12880,4571,142577,116754,345,255,27679,16870,0,0,5329,134701,333,25068,1447
 92 | 2025‑06‑05 20:03:49,12883,4573,142734,116894,345,255,27680,16870,0,0,5330,134818,334,25069,1447
 93 | 2025‑06‑05 20:14:36,12886,4573,142912,117029,345,255,27681,16873,0,0,5333,135000,335,25070,1447
 94 | 2025‑06‑05 21:00:58,12900,4585,143860,117767,351,259,27686,16875,0,0,5344,135780,338,25074,1448
 95 | 2025‑06‑06 00:02:01,12948,4605,147162,120471,388,289,27697,16882,0,0,5388,138826,349,25091,1451
 96 | 2025‑06‑06 03:21:44,12993,4631,149856,122901,405,302,27707,16890,0,0,5437,142186,362,25109,1454
 97 | 2025‑06‑06 06:01:09,13019,4645,151890,124619,411,306,27589,16774,0,0,5476,144869,372,25123,1456
 98 | 2025‑06‑06 09:01:17,12996,4627,154381,126720,415,313,27616,16793,0,0,5520,147899,384,25140,1459
 99 | 2025‑06‑06 12:01:16,13080,4678,157281,129172,423,318,27641,16799,0,0,5563,150928,395,25156,1462
100 | 2025‑06‑06 15:01:01,13148,4735,160574,132038,436,326,27656,16818,0,0,5607,153952,407,25172,1465
101 | 2025‑06‑06 18:00:59,13224,4781,164030,135074,441,329,27679,16837,0,0,5651,156980,418,25189,1468
102 | 2025‑06‑06 18:46:45,13243,4788,165292,136192,445,333,27685,16841,0,0,5662,157750,421,25193,1469
103 | 2025‑06‑06 18:50:47,13243,4788,165375,136268,445,333,27685,16843,0,0,5663,157804,421,25193,1469
104 | 2025‑06‑06 21:00:59,13292,4817,167406,138043,450,334,27697,16851,0,0,5692,159561,432,25203,1473
105 | 2025‑06‑07 00:02:10,13350,4861,170226,140491,520,345,27720,16861,0,0,5733,162005,446,25217,1477
106 | 2025‑06‑07 03:20:20,13406,4886,172624,142523,526,381,27740,16875,0,0,5778,164678,461,25232,1483
107 | 2025‑06‑07 06:00:56,13447,4919,174247,143894,540,390,27756,16881,0,0,5814,166844,474,25245,1487
108 | 2025‑06‑07 09:00:55,13511,4957,175935,145320,550,401,27772,16888,0,0,5855,169272,488,25259,1492
109 | 2025‑06‑07 12:01:01,13558,5006,178367,147424,559,411,27783,16898,0,0,5896,171701,502,25272,1497
110 | 2025‑06‑07 15:01:06,13611,5046,181055,149777,566,413,27794,16906,0,0,5937,174130,516,25286,1502
111 | 2025‑06‑07 18:00:57,13668,5086,183792,152091,592,439,27824,16932,0,0,5977,176556,531,25300,1506
112 | 2025‑06‑07 21:01:01,13726,5120,186943,154860,602,449,27786,16894,0,0,6018,178985,545,25314,1511
113 | 2025‑06‑08 00:02:05,13747,5147,189457,157017,626,477,27802,16903,0,0,6059,181427,559,25328,1516
114 | 2025‑06‑08 03:26:57,13786,5182,191642,158863,632,482,27801,16901,0,0,6105,184191,575,25343,1522
115 | 2025‑06‑08 06:01:04,13808,5198,193372,160278,644,488,27810,16908,0,0,6140,186270,587,25355,1526
116 | 2025‑06‑08 09:01:01,13855,5231,195169,161814,648,491,27829,16917,0,0,6181,188697,601,25369,1530
117 | 2025‑06‑08 12:01:02,13909,5282,197184,163613,653,500,27841,16930,0,0,6222,191125,615,25383,1535
118 | 2025‑06‑08 15:00:57,13960,5330,199786,165861,668,508,27857,16946,0,0,6262,193552,630,25397,1540
119 | 2025‑06‑08 18:00:54,14014,5372,202688,168437,678,518,27853,16943,0,0,6303,195979,644,25410,1545
120 | 2025‑06‑08 21:01:08,14074,5417,205798,171172,699,537,27874,16959,0,0,6344,198411,658,25424,1550
121 | 2025‑06‑09 00:01:58,14162,5489,207899,173018,704,543,27888,16967,0,0,6385,200850,672,25438,1555
122 | 2025‑06‑09 00:52:28,14168,5500,208687,173807,704,544,27896,16973,3750,1524,6396,201531,676,25442,1556
123 | 2025‑06‑09 00:56:33,14168,5502,208718,173850,705,545,27896,16973,3750,1524,6397,201586,676,25442,1556
124 | 2025‑06‑09 03:25:53,14228,5558,210702,175554,712,548,27896,16968,3752,1526,6439,203580,685,25457,1561
125 | 2025‑06‑09 06:01:04,14274,5595,212250,176851,717,552,27910,16979,3754,1526,6482,205652,694,25472,1565
126 | 2025‑06‑09 09:01:12,14323,5632,214486,178755,727,556,27929,16992,3756,1527,6532,208058,704,25490,1570
127 | 2025‑06‑09 12:01:11,14326,5669,216838,180780,737,566,27944,17000,3767,1531,6582,210462,715,25507,1576
128 | 2025‑06‑09 14:38:49,14382,5708,219410,183024,745,573,27965,17014,3776,1536,6626,212567,724,25522,1580
129 | 2025‑06‑09 15:01:08,14392,5714,219792,183328,746,573,27969,17016,3778,1537,6633,212865,725,25525,1581
130 | 2025‑06‑09 18:01:07,14497,5785,223178,186266,764,586,27988,17026,3794,1546,6683,215269,735,25542,1586
131 | 2025‑06‑09 21:01:07,14573,5830,226196,188954,782,597,28013,17050,3823,1559,6733,217673,746,25560,1592
132 | 2025‑06‑10 00:02:00,14638,5867,228545,191005,795,608,28045,17078,3844,1568,6783,220088,756,25577,1597
133 | 2025‑06‑10 03:23:13,14660,5895,230792,192872,805,614,28044,17068,3867,1585,6840,222776,768,25597,1603
134 | 2025‑06‑10 06:01:01,14693,5938,232680,194475,821,622,28056,17079,3883,1599,6883,224883,777,25612,1608
135 | 2025‑06‑10 09:01:55,14738,5986,234733,196223,836,634,28080,17095,3895,1612,6934,227299,787,25630,1613
136 | 2025‑06‑10 12:01:23,14634,6025,237435,198531,847,644,28118,17133,3901,1614,6984,229696,798,25647,1618
137 | 2025‑06‑10 15:01:15,14703,6072,240364,201100,859,649,28146,17158,3907,1617,7034,232098,808,25665,1623
138 | 2025‑06‑10 18:01:12,14790,6127,243661,203958,874,656,28171,17173,3910,1617,7084,234501,818,25682,1629
139 | 2025‑06‑10 21:01:17,14921,6207,246577,206404,889,670,28204,17205,3918,1624,7134,236906,829,25700,1634
140 | 2025‑06‑11 00:02:05,15000,6257,249119,208528,903,677,28229,17213,3926,1631,7185,239321,839,25717,1639
141 | 2025‑06‑11 03:22:41,15078,6302,251304,210456,912,683,28157,17141,3931,1636,7241,242000,851,25737,1645
142 | 2025‑06‑11 06:01:07,15172,6365,253021,211954,921,691,28178,17152,3935,1637,7285,244116,860,25752,1650
143 | 2025‑06‑11 09:01:37,15229,6391,255138,213747,927,697,28205,17181,3938,1638,7336,246462,869,25776,1654
144 | 2025‑06‑11 12:01:15,15284,6425,257837,216098,927,697,28233,17202,3939,1640,7387,248797,878,25800,1659
145 | 2025‑06‑11 15:01:03,15378,6489,260737,218669,935,702,28230,17203,3940,1638,7438,251135,887,25823,1663
146 | 2025‑06‑11 18:01:07,15462,6546,263509,221046,952,712,28213,17184,3971,1660,7489,253475,896,25847,1667
147 | 2025‑06‑11 21:01:14,15561,6607,266480,223695,964,720,28226,17186,3991,1679,7541,255817,905,25871,1671
148 | 2025‑06‑12 00:02:07,15664,6675,268861,225836,977,729,28249,17205,4028,1704,7592,258168,914,25895,1676
149 | 2025‑06‑12 03:21:37,15704,6709,271237,227923,983,735,28283,17227,4031,1711,7649,260761,924,25921,1680
150 | 2025‑06‑12 06:01:00,15728,6743,273103,229529,988,739,28317,17253,4031,1711,7694,262833,932,25942,1684
151 | 2025‑06‑12 09:01:02,15768,6794,275293,231473,998,747,28350,17276,4048,1719,7745,265173,941,25966,1688
152 | 2025‑06‑12 12:01:10,15794,6824,277604,233461,1008,753,28350,17293,4075,1730,7796,267515,950,25990,1692
153 | 2025‑06‑12 15:01:03,15904,6878,280944,236477,1018,759,28378,17314,4082,1733,7847,269853,960,26014,1697
154 | 2025‑06‑12 18:01:02,16024,6936,283522,238719,1025,760,28431,17341,4089,1737,7898,272193,969,26038,1701
155 | 2025‑06‑12 21:01:11,16092,6973,285938,240838,1030,765,28449,17344,4095,1742,7949,274535,978,26061,1705
156 | 2025‑06‑13 00:02:05,16150,7022,287960,242623,1032,772,28461,17349,4113,1756,8001,276886,987,26085,1710
157 | 2025‑06‑13 03:22:50,16279,7080,289986,244417,1039,779,28468,17354,4123,1763,8058,279496,997,26112,1714
158 | 2025‑06‑13 06:01:09,16352,7168,291532,245796,1040,781,28477,17361,4129,1766,8103,281554,1005,26133,1718
159 | 2025‑06‑13 09:01:04,16521,7321,293311,247344,1050,785,28509,17385,4067,1713,8154,283892,1014,26157,1722
160 | 2025‑06‑13 12:01:10,16562,7351,295753,249575,1062,795,28535,17406,4072,1719,8205,286233,1023,26180,1726
161 | 2025‑06‑13 15:01:14,16664,7407,298105,251566,1075,806,28563,17428,4045,1695,8256,288574,1032,26204,1731
162 | 2025‑06‑13 18:01:14,16721,7457,299655,252892,1093,828,28597,17448,4048,1699,8307,290914,1041,26228,1735
163 | 2025‑06‑13 21:00:57,16813,7514,302297,255225,1103,831,28613,17461,4050,1702,8353,292845,1048,26243,1737
164 | 2025‑06‑14 00:02:04,16896,7580,304282,257019,1108,835,28629,17470,4055,1702,8398,294792,1054,26258,1740
165 | 2025‑06‑14 03:19:32,16946,7625,306397,258872,1114,839,28629,17469,4064,1706,8448,296914,1062,26274,1742
166 | 2025‑06‑14 06:01:07,16968,7652,308044,260337,1116,841,28651,17485,4066,1706,8489,298651,1068,26288,1745
167 | 2025‑06‑14 09:00:55,16975,7685,309445,261604,1125,847,28603,17446,4068,1706,8535,300583,1074,26303,1747
168 | 2025‑06‑14 12:01:06,15433,7719,310888,262842,1127,848,28604,17455,4073,1706,8581,302520,1081,26318,1749
169 | 2025‑06‑14 15:01:04,15545,7781,312833,264593,1133,854,28622,17467,4074,1707,8626,304454,1088,26333,1752
170 | 2025‑06‑14 18:01:07,15654,7840,315101,266636,1136,858,28635,17481,4078,1709,8672,306389,1095,26348,1754
171 | 2025‑06‑14 21:01:04,15727,7914,317599,268843,1145,865,28614,17456,4087,1713,8717,308323,1101,26363,1757
172 | 2025‑06‑15 00:02:07,15751,7930,319978,270953,1148,869,28647,17463,4103,1729,8763,310268,1108,26378,1759
173 | 2025‑06‑15 03:27:22,15809,7968,321493,272315,1159,879,28622,17461,4121,1742,8815,312474,1116,26395,1762
174 | 2025‑06‑15 06:00:56,15837,7993,322513,273180,1169,885,28626,17465,4141,1761,8854,314125,1121,26407,1764
175 | 2025‑06‑15 09:01:15,15880,8027,324694,274974,1177,887,28656,17482,4155,1770,8900,316063,1128,26422,1766
176 | 2025‑06‑15 12:01:11,15955,8062,326758,276578,1181,890,28669,17489,4159,1772,8945,317996,1135,26437,1769
177 | 2025‑06‑15 15:01:02,16057,8145,328798,278371,1188,894,28675,17494,4165,1773,8991,319929,1141,26452,1771
178 | 2025‑06‑15 18:01:06,16150,8196,331624,280913,1206,910,28683,17500,4169,1775,9037,321865,1148,26467,1773
179 | 2025‑06‑15 21:01:00,16294,8292,333834,282865,1201,905,28731,17542,4169,1778,9082,323798,1155,26482,1776
180 | 2025‑06‑16 00:36:04,16353,8341,336517,285170,1212,916,28739,17544,4097,1708,9137,326109,1163,26500,1779
181 | 2025‑06‑16 03:25:53,16397,8364,338382,286750,1226,926,28750,17553,4091,1707,9180,327934,1169,26514,1781
182 | 2025‑06‑16 06:01:20,16448,8406,339735,287933,1233,930,28785,17570,4086,1703,9219,329605,1175,26527,1783
183 | 2025‑06‑16 09:01:44,16539,8493,340692,288582,1234,932,28819,17593,4091,1703,9264,331763,1184,26549,1786
184 | 2025‑06‑16 12:01:12,16597,8544,342642,290296,1250,945,28834,17606,4094,1704,9309,333911,1193,26571,1789
185 | 2025‑06‑16 15:01:09,16734,8627,345485,292795,1255,949,28862,17615,3888,1536,9354,336064,1202,26593,1792
186 | 2025‑06‑16 18:01:09,16808,8681,348101,295086,1267,955,28904,17656,3889,1537,9399,338217,1210,26615,1795
187 | 2025‑06‑16 21:01:08,16866,8740,351381,298007,1275,955,28922,17664,3890,1537,9444,340371,1219,26637,1798
188 | 2025‑06‑17 00:02:03,16925,8785,353453,299852,1284,962,28973,17705,3898,1541,9489,342535,1228,26659,1801
189 | 2025‑06‑17 03:22:49,16989,8828,355433,301635,1292,970,28951,17709,3911,1547,9540,344937,1238,26683,1804
190 | 2025‑06‑17 06:01:08,17011,8835,357049,303053,1298,975,28987,17726,3927,1559,9579,346832,1246,26703,1807
191 | 2025‑06‑17 09:01:57,17066,8887,358256,304089,1303,978,29001,17736,3929,1561,9625,348995,1255,26725,1810
192 | 2025‑06‑17 12:01:13,17114,8927,360443,306022,1322,987,29014,17740,3930,1563,9669,351140,1263,26746,1812
193 | 2025‑06‑17 15:01:12,17217,8993,362959,308299,1333,995,29028,17751,3934,1564,9715,353293,1272,26768,1815
194 | 2025‑06‑17 18:01:00,17270,9061,366148,311188,1346,1003,29069,17759,3936,1565,9760,355445,1281,26790,1818
195 | 2025‑06‑17 21:01:09,17212,8993,367732,312552,1350,1010,29093,17771,3940,1565,9805,357600,1290,26812,1821
196 | 2025‑06‑18 00:01:56,17218,8982,369603,314239,1364,1014,29111,17778,3946,1566,9850,359763,1299,26834,1824
197 | 2025‑06‑18 03:22:07,17259,9001,371316,315681,1371,1016,29129,17782,3948,1566,9900,362158,1309,26859,1828
198 | 2025‑06‑18 06:01:13,17349,9032,371694,315997,1379,1023,29135,17788,3950,1567,9940,364062,1317,26878,1830
199 | 2025‑06‑18 09:01:23,17405,9081,373578,317614,1388,1026,29145,17790,3952,1568,9985,366217,1325,26900,1833
200 | 2025‑06‑18 12:01:14,17497,9133,375611,319400,1396,1034,29161,17796,3953,1568,10030,368369,1334,26922,1836
201 | 2025‑06‑18 15:01:05,17579,9206,378611,322064,1390,1036,29170,17806,3955,1569,10075,370521,1343,26944,1839
202 | 2025‑06‑18 18:01:12,17680,9263,381218,324371,1401,1045,29222,17843,3958,1570,10120,372676,1352,26966,1842
203 | 2025‑06‑18 21:00:59,17748,9303,384910,327928,1414,1052,29235,17852,3965,1572,10156,374729,1369,26989,1846
204 | 2025‑06‑19 00:02:02,17815,9343,387324,330081,1422,1059,29232,17852,3968,1574,10191,376796,1387,27012,1850
205 | 2025‑06‑19 03:22:23,17893,9386,389285,331859,1436,1069,29260,17872,3969,1576,10231,379084,1406,27038,1855
206 | 2025‑06‑19 06:01:03,17939,9419,391110,333458,1447,1078,29281,17887,3981,1585,10262,380896,1421,27058,1858
207 | 2025‑06‑19 09:01:14,18022,9468,392555,334699,1457,1085,29291,17895,3997,1598,10298,382953,1439,27081,1863
208 | 2025‑06‑19 12:01:17,18084,9499,395472,337346,1465,1089,29314,17901,4007,1603,10334,385009,1456,27104,1867
209 | 2025‑06‑19 15:01:09,18155,9554,397696,339372,1476,1099,29344,17917,4011,1603,10369,387063,1473,27127,1871
210 | 2025‑06‑19 18:00:57,18255,9624,400057,341541,1484,1107,29362,17926,4007,1601,10405,389116,1491,27150,1875
211 | 2025‑06‑19 21:01:04,18333,9676,402858,344083,1503,1118,29385,17945,4014,1604,10440,391173,1508,27173,1879
212 | 2025‑06‑20 00:02:05,18388,9720,404907,345886,1532,1130,29390,17958,4010,1602,10476,393240,1526,27197,1883
213 | 2025‑06‑20 03:22:05,17542,9750,407380,348064,1562,1152,29423,17985,3999,1596,10516,395524,1545,27222,1888
214 | 2025‑06‑20 06:01:10,17401,9761,408599,349168,1576,1163,29448,17996,3999,1596,10547,397340,1560,27243,1891
215 | 2025‑06‑20 09:01:03,17449,9796,410135,350541,1598,1183,29486,18025,4001,1599,10583,399395,1578,27266,1895
216 | 2025‑06‑20 12:01:04,17339,9820,411911,352096,1627,1205,29514,18043,4004,1599,10618,401450,1595,27289,1899
217 | 2025‑06‑20 15:01:09,17410,9859,413763,353639,1653,1212,29534,18056,4013,1602,10654,403506,1612,27312,1904
218 | 2025‑06‑20 18:01:12,17464,9894,415905,355473,1681,1235,29619,18103,4006,1599,10690,405562,1630,27335,1908
219 | 2025‑06‑20 21:00:56,17352,9914,418244,357621,1721,1256,29653,18125,4017,1603,10725,407615,1647,27358,1912
220 | 2025‑06‑21 00:05:29,17274,9946,420458,359609,1729,1266,29617,18077,4027,1613,10762,409722,1665,27381,1916
221 | 2025‑06‑21 03:20:36,17336,9969,421881,360882,1755,1288,29634,18092,4028,1613,10800,411950,1684,27406,1920
222 | 2025‑06‑21 06:01:01,17365,9990,423298,362137,1757,1289,29645,18101,4029,1609,10832,413782,1699,27427,1924
223 | 2025‑06‑21 09:00:59,17410,10025,425081,363903,1766,1302,29658,18109,4032,1611,10860,415684,1716,27442,1930
224 | 2025‑06‑21 12:01:05,17479,10046,427259,365849,1783,1313,29678,18124,4037,1611,10888,417586,1734,27457,1935
225 | 2025‑06‑21 15:01:05,17529,10074,428555,366999,1804,1331,29709,18147,4042,1613,10916,419488,1751,27472,1941
226 | 2025‑06‑21 18:00:53,17596,10124,430896,369092,1831,1355,29734,18166,4046,1614,10944,421388,1768,27487,1946
227 | 2025‑06‑21 21:01:03,17648,10159,433612,371519,1850,1371,29752,18173,4047,1616,10973,423292,1786,27503,1952
228 | 2025‑06‑22 00:02:05,17581,10197,435356,373069,1863,1379,29757,18174,4048,1616,11001,425204,1803,27518,1957
229 | 2025‑06‑22 03:27:34,17579,10200,437065,374611,1875,1388,29771,18186,4050,1616,11033,427376,1823,27535,1964
230 | 2025‑06‑22 06:01:00,17605,10218,438511,375917,1890,1399,29791,18195,4056,1617,11057,428997,1838,27548,1968
231 | 2025‑06‑22 09:01:10,17633,10244,438638,375842,1906,1412,29808,18204,4058,1617,11085,430900,1855,27563,1974
232 | 2025‑06‑22 12:01:14,17674,10277,440328,377318,1925,1419,29819,18210,4061,1621,11113,432803,1873,27578,1980
233 | 2025‑06‑22 14:11:04,17683,10297,441856,378659,1935,1426,29836,18214,4063,1621,11134,434175,1885,27589,1984
234 | 2025‑06‑22 14:18:25,17683,10301,441948,378741,1936,1427,29836,18215,4063,1621,11135,434252,1886,27590,1984
235 | 2025‑06‑22 14:27:21,17684,10302,442041,378841,1936,1427,29837,18215,4063,1621,11136,434347,1887,27591,1984
236 | 2025‑06‑22 14:34:22,17688,10305,442239,378998,1934,1425,29837,18215,4063,1621,11137,434421,1888,27591,1984
237 | 2025‑06‑22 15:00:57,17692,10309,442525,379255,1935,1425,29839,18219,4063,1621,11141,434702,1890,27593,1985
238 | 2025‑06‑22 18:00:56,17733,10343,444857,381318,1966,1449,29855,18235,4063,1621,11169,436603,1907,27608,1991
239 | 2025‑06‑22 21:00:58,17781,10366,447148,383378,1996,1474,29851,18236,4068,1624,11198,438506,1925,27624,1996
240 | 2025‑06‑23 00:02:01,17819,10395,449106,385095,2022,1497,29862,18245,4090,1643,11226,440419,1942,27639,2002
241 | 2025‑06‑23 03:27:58,17818,10398,449533,385377,2040,1510,29878,18251,4099,1651,11258,442595,1962,27656,2008
242 | 2025‑06‑23 06:01:08,17838,10413,450439,386193,2046,1514,29891,18254,4110,1657,11282,444213,1977,27669,2013
243 | 2025‑06‑23 09:02:05,17855,10430,452018,387521,2056,1521,29832,18196,4120,1668,11312,446177,1998,27694,2016
244 | 2025‑06‑23 12:01:08,17890,10453,454235,389610,2077,1534,29864,18217,4124,1669,11341,448121,2019,27718,2019
245 | 2025‑06‑23 13:52:45,17941,10474,455538,390764,2089,1542,29888,18234,4124,1669,11360,449333,2032,27733,2020
246 | 2025‑06‑23 14:28:51,17949,10478,456088,391253,2083,1536,29892,18238,4124,1669,11366,449725,2036,27738,2021
247 | 2025‑06‑23 15:01:15,17960,10481,456562,391652,2089,1541,29901,18243,4124,1669,11371,450077,2040,27742,2022
248 | 2025‑06‑23 18:01:09,18018,10509,459296,394160,2123,1566,29943,18263,4119,1666,11401,452030,2061,27767,2024
249 | 2025‑06‑23 21:00:58,17991,10533,461799,396385,2162,1590,29986,18299,4120,1666,11430,453982,2082,27791,2027
250 | 2025‑06‑24 00:01:56,18073,10560,463547,397979,2189,1612,30018,18321,4122,1667,11460,455947,2103,27816,2030
251 | 2025‑06‑24 03:23:44,18146,10595,465461,399635,2233,1647,30042,18333,4123,1667,11493,458138,2127,27843,2033
252 | 2025‑06‑24 06:01:19,18172,10604,467104,401072,2244,1657,30068,18353,4124,1667,11519,459848,2145,27865,2036
253 | 2025‑06‑24 09:01:55,18224,10639,468319,402134,2260,1662,30090,18362,4126,1667,11549,461809,2167,27889,2039
254 | 2025‑06‑24 12:01:08,18260,10655,470129,403715,2281,1686,30099,18368,4129,1668,11579,463755,2188,27913,2041
255 | 2025‑06‑24 15:01:16,18298,10678,472189,405528,2282,1684,30123,18379,4129,1668,11608,465710,2209,27938,2044
256 | 2025‑06‑24 18:01:22,18392,10725,474523,407669,2311,1708,30154,18390,4129,1668,11638,467666,2230,27962,2047
257 | 2025‑06‑24 21:01:03,18402,10776,476974,409770,2331,1723,30167,18400,4130,1668,11668,469616,2251,27987,2050
258 | 2025‑06‑25 00:02:07,18472,10820,478514,411118,2351,1733,30182,18408,4157,1685,11697,471582,2272,28011,2053
259 | 2025‑06‑25 03:24:23,18348,10848,480327,412670,2370,1746,30206,18428,4164,1691,11731,473778,2296,28039,2056
260 | 2025‑06‑25 06:01:06,18040,10885,481483,413704,2383,1758,30210,18443,4164,1691,11757,475479,2314,28060,2058
261 | 2025‑06‑25 09:02:18,17924,10903,482910,414966,2409,1773,30197,18427,4164,1692,11787,477447,2335,28085,2061
262 | 2025‑06‑25 12:01:07,17971,10929,485083,416935,2435,1789,30228,18449,4165,1692,11816,479388,2356,28109,2064
263 | 2025‑06‑25 14:01:25,18033,10964,486773,418428,2444,1798,28969,18469,4148,1692,11845,480885,2364,28131,2065
264 | 2025‑06‑25 14:09:09,18035,10967,486876,418524,2444,1798,28970,18470,4148,1692,11848,480989,2364,28133,2065
265 | 2025‑06‑25 14:17:58,18042,10969,486993,418625,2447,1801,28970,18471,4148,1692,11849,481103,2367,28133,2065
266 | 2025‑06‑26 18:14:49,18744,11336,502355,432522,2659,1950,29122,18578,4163,1695,12268,496301,2572,28274,2079
267 | 


--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | prarena.ai


--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | title: AI PR Watcher
3 | description: Tracking Copilot vs Codex PR performance
4 | ---


--------------------------------------------------------------------------------
/docs/chart-data.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "labels": [
  3 |     "05/26 14:21",
  4 |     "06/02 06:01",
  5 |     "06/09 00:01",
  6 |     "06/16 03:25",
  7 |     "06/23 03:27",
  8 |     "07/01 12:01",
  9 |     "07/08 21:01",
 10 |     "07/16 09:03"
 11 |   ],
 12 |   "datasets": [
 13 |     {
 14 |       "label": "Copilot Total",
 15 |       "type": "bar",
 16 |       "data": [
 17 |         8729,
 18 |         11274,
 19 |         14162,
 20 |         16397,
 21 |         17818,
 22 |         22119,
 23 |         29912,
 24 |         39729
 25 |       ],
 26 |       "backgroundColor": "#93c5fd",
 27 |       "borderColor": "#93c5fd",
 28 |       "borderWidth": 1,
 29 |       "yAxisID": "y",
 30 |       "order": 2
 31 |     },
 32 |     {
 33 |       "label": "Copilot Merged",
 34 |       "type": "bar",
 35 |       "data": [
 36 |         1594,
 37 |         3534,
 38 |         5489,
 39 |         8364,
 40 |         10398,
 41 |         12953,
 42 |         17022,
 43 |         22200
 44 |       ],
 45 |       "backgroundColor": "#2563eb",
 46 |       "borderColor": "#2563eb",
 47 |       "borderWidth": 1,
 48 |       "yAxisID": "y",
 49 |       "order": 2
 50 |     },
 51 |     {
 52 |       "label": "Copilot Success % (Ready)",
 53 |       "type": "line",
 54 |       "data": [
 55 |         75.94092424964268,
 56 |         85.05415162454874,
 57 |         85.7790279731208,
 58 |         91.01196953210011,
 59 |         92.01769911504425,
 60 |         92.52803771697978,
 61 |         92.02573390279505,
 62 |         91.68628422748111
 63 |       ],
 64 |       "borderColor": "#1d4ed8",
 65 |       "backgroundColor": "rgba(255, 255, 255, 0.8)",
 66 |       "borderWidth": 3,
 67 |       "pointRadius": 3,
 68 |       "pointHoverRadius": 5,
 69 |       "fill": false,
 70 |       "yAxisID": "y1",
 71 |       "order": 1,
 72 |       "rateType": "ready"
 73 |     },
 74 |     {
 75 |       "label": "Copilot Success % (All)",
 76 |       "type": "line",
 77 |       "data": [
 78 |         18.260969183182496,
 79 |         31.34646088344864,
 80 |         38.75864990820506,
 81 |         51.00933097517839,
 82 |         58.356717925693125,
 83 |         58.560513585605136,
 84 |         56.90692698582509,
 85 |         55.87857736162501
 86 |       ],
 87 |       "borderColor": "#1d4ed8",
 88 |       "backgroundColor": "rgba(255, 255, 255, 0.8)",
 89 |       "borderWidth": 3,
 90 |       "pointRadius": 3,
 91 |       "pointHoverRadius": 5,
 92 |       "fill": false,
 93 |       "yAxisID": "y1",
 94 |       "order": 1,
 95 |       "hidden": true,
 96 |       "rateType": "total"
 97 |     },
 98 |     {
 99 |       "label": "Codex Total",
100 |       "type": "bar",
101 |       "data": [
102 |         54211,
103 |         77017,
104 |         207899,
105 |         338382,
106 |         449533,
107 |         560343,
108 |         645347,
109 |         718990
110 |       ],
111 |       "backgroundColor": "#fca5a5",
112 |       "borderColor": "#fca5a5",
113 |       "borderWidth": 1,
114 |       "yAxisID": "y",
115 |       "order": 2
116 |     },
117 |     {
118 |       "label": "Codex Merged",
119 |       "type": "bar",
120 |       "data": [
121 |         44230,
122 |         64142,
123 |         173018,
124 |         286750,
125 |         385377,
126 |         484317,
127 |         559761,
128 |         625691
129 |       ],
130 |       "backgroundColor": "#dc2626",
131 |       "borderColor": "#dc2626",
132 |       "borderWidth": 1,
133 |       "yAxisID": "y",
134 |       "order": 2
135 |     },
136 |     {
137 |       "label": "Codex Success % (Ready)",
138 |       "type": "line",
139 |       "data": [
140 |         85.80352293008458,
141 |         86.4610573423557,
142 |         86.2334840185607,
143 |         87.4240470245336,
144 |         87.4969916857004,
145 |         87.45388653242975,
146 |         87.7211595420254,
147 |         87.96801781599419
148 |       ],
149 |       "borderColor": "#b91c1c",
150 |       "backgroundColor": "rgba(255, 255, 255, 0.8)",
151 |       "borderWidth": 3,
152 |       "pointRadius": 3,
153 |       "pointHoverRadius": 5,
154 |       "fill": false,
155 |       "yAxisID": "y1",
156 |       "order": 1,
157 |       "rateType": "ready"
158 |     },
159 |     {
160 |       "label": "Codex Success % (All)",
161 |       "type": "line",
162 |       "data": [
163 |         81.58860747818709,
164 |         83.28291156497916,
165 |         83.22214152064224,
166 |         84.74150516280416,
167 |         85.72830025826802,
168 |         86.43223882514816,
169 |         86.73798747030668,
170 |         87.02360255358211
171 |       ],
172 |       "borderColor": "#b91c1c",
173 |       "backgroundColor": "rgba(255, 255, 255, 0.8)",
174 |       "borderWidth": 3,
175 |       "pointRadius": 3,
176 |       "pointHoverRadius": 5,
177 |       "fill": false,
178 |       "yAxisID": "y1",
179 |       "order": 1,
180 |       "hidden": true,
181 |       "rateType": "total"
182 |     },
183 |     {
184 |       "label": "Cursor Total",
185 |       "type": "bar",
186 |       "data": [
187 |         null,
188 |         null,
189 |         704,
190 |         1226,
191 |         2040,
192 |         3931,
193 |         11390,
194 |         19129
195 |       ],
196 |       "backgroundColor": "#c4b5fd",
197 |       "borderColor": "#c4b5fd",
198 |       "borderWidth": 1,
199 |       "yAxisID": "y",
200 |       "order": 2
201 |     },
202 |     {
203 |       "label": "Cursor Merged",
204 |       "type": "bar",
205 |       "data": [
206 |         null,
207 |         null,
208 |         543,
209 |         926,
210 |         1510,
211 |         2848,
212 |         8587,
213 |         14436
214 |       ],
215 |       "backgroundColor": "#7c3aed",
216 |       "borderColor": "#7c3aed",
217 |       "borderWidth": 1,
218 |       "yAxisID": "y",
219 |       "order": 2
220 |     },
221 |     {
222 |       "label": "Cursor Success % (Ready)",
223 |       "type": "line",
224 |       "data": [
225 |         null,
226 |         null,
227 |         80.56379821958457,
228 |         78.40812870448772,
229 |         76.26262626262627,
230 |         74.4186046511628,
231 |         76.34924868853916,
232 |         76.1633428300095
233 |       ],
234 |       "borderColor": "#6d28d9",
235 |       "backgroundColor": "rgba(255, 255, 255, 0.8)",
236 |       "borderWidth": 3,
237 |       "pointRadius": 3,
238 |       "pointHoverRadius": 5,
239 |       "fill": false,
240 |       "yAxisID": "y1",
241 |       "order": 1,
242 |       "rateType": "ready"
243 |     },
244 |     {
245 |       "label": "Cursor Success % (All)",
246 |       "type": "line",
247 |       "data": [
248 |         null,
249 |         null,
250 |         77.13068181818183,
251 |         75.53017944535073,
252 |         74.01960784313727,
253 |         72.44975833121343,
254 |         75.39069359086919,
255 |         75.46656908359036
256 |       ],
257 |       "borderColor": "#6d28d9",
258 |       "backgroundColor": "rgba(255, 255, 255, 0.8)",
259 |       "borderWidth": 3,
260 |       "pointRadius": 3,
261 |       "pointHoverRadius": 5,
262 |       "fill": false,
263 |       "yAxisID": "y1",
264 |       "order": 1,
265 |       "hidden": true,
266 |       "rateType": "total"
267 |     },
268 |     {
269 |       "label": "Devin Total",
270 |       "type": "bar",
271 |       "data": [
272 |         null,
273 |         27290,
274 |         27888,
275 |         28750,
276 |         29878,
277 |         29458,
278 |         30439,
279 |         31211
280 |       ],
281 |       "backgroundColor": "#86efac",
282 |       "borderColor": "#86efac",
283 |       "borderWidth": 1,
284 |       "yAxisID": "y",
285 |       "order": 2
286 |     },
287 |     {
288 |       "label": "Devin Merged",
289 |       "type": "bar",
290 |       "data": [
291 |         null,
292 |         16671,
293 |         16967,
294 |         17553,
295 |         18251,
296 |         18806,
297 |         19471,
298 |         19908
299 |       ],
300 |       "backgroundColor": "#059669",
301 |       "borderColor": "#059669",
302 |       "borderWidth": 1,
303 |       "yAxisID": "y",
304 |       "order": 2
305 |     },
306 |     {
307 |       "label": "Devin Success % (Ready)",
308 |       "type": "line",
309 |       "data": [
310 |         null,
311 |         68.0115861618799,
312 |         66.71516200062912,
313 |         66.33786848072563,
314 |         66.06218554312811,
315 |         65.7575439700689,
316 |         65.94526857684752,
317 |         65.80504412785508
318 |       ],
319 |       "borderColor": "#047857",
320 |       "backgroundColor": "rgba(255, 255, 255, 0.8)",
321 |       "borderWidth": 3,
322 |       "pointRadius": 3,
323 |       "pointHoverRadius": 5,
324 |       "fill": false,
325 |       "yAxisID": "y1",
326 |       "order": 1,
327 |       "rateType": "ready"
328 |     },
329 |     {
330 |       "label": "Devin Success % (All)",
331 |       "type": "line",
332 |       "data": [
333 |         null,
334 |         61.08831073653352,
335 |         60.83978772231784,
336 |         61.05391304347826,
337 |         61.08507932257849,
338 |         63.84004345169394,
339 |         63.96727881993495,
340 |         63.78520393451027
341 |       ],
342 |       "borderColor": "#047857",
343 |       "backgroundColor": "rgba(255, 255, 255, 0.8)",
344 |       "borderWidth": 3,
345 |       "pointRadius": 3,
346 |       "pointHoverRadius": 5,
347 |       "fill": false,
348 |       "yAxisID": "y1",
349 |       "order": 1,
350 |       "hidden": true,
351 |       "rateType": "total"
352 |     },
353 |     {
354 |       "label": "Codegen Total",
355 |       "type": "bar",
356 |       "data": [
357 |         null,
358 |         null,
359 |         null,
360 |         4091,
361 |         4099,
362 |         4129,
363 |         4242,
364 |         4462
365 |       ],
366 |       "backgroundColor": "#fed7aa",
367 |       "borderColor": "#fed7aa",
368 |       "borderWidth": 1,
369 |       "yAxisID": "y",
370 |       "order": 2
371 |     },
372 |     {
373 |       "label": "Codegen Merged",
374 |       "type": "bar",
375 |       "data": [
376 |         null,
377 |         null,
378 |         null,
379 |         1523,
380 |         1655,
381 |         1667,
382 |         1727,
383 |         1847
384 |       ],
385 |       "backgroundColor": "#d97706",
386 |       "borderColor": "#d97706",
387 |       "borderWidth": 1,
388 |       "yAxisID": "y",
389 |       "order": 2
390 |     },
391 |     {
392 |       "label": "Codegen Success % (Ready)",
393 |       "type": "line",
394 |       "data": [
395 |         null,
396 |         null,
397 |         null,
398 |         85.46576879910214,
399 |         82.70864567716141,
400 |         80.25999037072701,
401 |         78.85844748858447,
402 |         77.11899791231733
403 |       ],
404 |       "borderColor": "#b45309",
405 |       "backgroundColor": "rgba(255, 255, 255, 0.8)",
406 |       "borderWidth": 3,
407 |       "pointRadius": 3,
408 |       "pointHoverRadius": 5,
409 |       "fill": false,
410 |       "yAxisID": "y1",
411 |       "order": 1,
412 |       "rateType": "ready"
413 |     },
414 |     {
415 |       "label": "Codegen Success % (All)",
416 |       "type": "line",
417 |       "data": [
418 |         null,
419 |         null,
420 |         null,
421 |         37.22806159863114,
422 |         40.37570139058307,
423 |         40.37297166384112,
424 |         40.711928335690715,
425 |         41.393993724787094
426 |       ],
427 |       "borderColor": "#b45309",
428 |       "backgroundColor": "rgba(255, 255, 255, 0.8)",
429 |       "borderWidth": 3,
430 |       "pointRadius": 3,
431 |       "pointHoverRadius": 5,
432 |       "fill": false,
433 |       "yAxisID": "y1",
434 |       "order": 1,
435 |       "hidden": true,
436 |       "rateType": "total"
437 |     }
438 |   ]
439 | }


--------------------------------------------------------------------------------
/docs/chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aavetis/ai-pr-watcher/af5a6c13cefaf86fe67f3339a542367961fee94b/docs/chart.png


--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
  1 | <!DOCTYPE html>
  2 | <html lang="en">
  3 |   <head>
  4 |     <meta charset="UTF-8" />
  5 |     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6 |     <title>PR Arena - AI Coding Agent Leaderboard</title>
  7 |     <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📊</text></svg>">
  8 |     <link rel="stylesheet" href="styles.css" />
  9 |     <meta http-equiv="refresh" content="3600" />
 10 |     <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
 11 |     <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
 12 |     <link rel="preconnect" href="https://fonts.googleapis.com" />
 13 |     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
 14 |     <link
 15 |       href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;1,400&family=IBM+Plex+Mono:wght@300;400;500;600&display=swap"
 16 |       rel="stylesheet"
 17 |     />
 18 | 
 19 |     <!-- Microsoft Clarity Analytics -->
 20 |     <script type="text/javascript">
 21 |         (function(c,l,a,r,i,t,y){
 22 |             c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
 23 |             t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
 24 |             y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
 25 |         })(window, document, "clarity", "script", "s40u7bxxnn");
 26 |     </script>
 27 |   </head>
 28 |   <body>
 29 |     <div class="container">
 30 |       <header class="hero">
 31 |         <h1>PR Arena</h1>
 32 |         <p>Software engineering agents head to head</p>
 33 |       </header>
 34 | 
 35 |       <!-- Dynamic Leaderboard -->
 36 |       <section class="leaderboard">
 37 |         <div class="leaderboard-header">
 38 |           <h2>Leaderboard</h2>
 39 |           <div class="sort-controls">
 40 |             <div class="dropdown-container">
 41 |               <button class="sort-btn dropdown-btn" data-sort="success-rate" id="successRateBtn">
 42 |                 Success Rate <span class="mode-indicator">(M/R)</span> <span class="dropdown-arrow">▼</span>
 43 |               </button>
 44 |               <div class="dropdown-menu" id="successRateDropdown">
 45 |                 <button class="dropdown-item active" data-rate-type="ready">Merged/Ready PRs</button>
 46 |                 <button class="dropdown-item" data-rate-type="total">Merged/All PRs, including drafts</button>
 47 |               </div>
 48 |             </div>
 49 |             <button class="sort-btn active" data-sort="total-prs">Total PRs</button>
 50 |             <button class="sort-btn" data-sort="merged-prs">Merged PRs</button>
 51 |           </div>
 52 |         </div>
 53 | 
 54 |         <div class="agents-container" id="agentsContainer">
 55 | 
 56 |           <div class="agent"
 57 |                data-success-rate="91.68628422748111"
 58 |                data-success-rate-ready="91.68628422748111"
 59 |                data-success-rate-total="55.87857736162501"
 60 |                data-total-prs="39729"
 61 |                data-ready-prs="24213"
 62 |                data-merged-prs="22200"
 63 |                data-agent-name="GitHub Copilot">
 64 |             <div class="agent-header">
 65 |               <div class="rank" data-rank="1">#1</div>
 66 |               <div class="agent-dot" style="background-color: #2563eb"></div>
 67 |               <div class="agent-info">
 68 |                 <h3><a href="https://docs.github.com/en/copilot/using-github-copilot/coding-agent/using-copilot-to-work-on-an-issue" target="_blank">GitHub Copilot</a></h3>
 69 |                 <span class="agent-subtitle">
 70 |                   <span class="pr-count">24,213</span>
 71 |                   <span class="pr-type">ready</span> /
 72 |                   <span class="pr-total">39,729</span>
 73 |                   <span class="pr-total-label">all PRs</span>
 74 |                 </span>
 75 |               </div>
 76 |             </div>
 77 |             <div class="metrics">
 78 |               <div class="primary-metric">
 79 |                 <span class="metric-value">
 80 |                   91.7%
 81 |                 </span>
 82 |                 <span class="metric-label">Success Rate</span>
 83 |               </div>
 84 |               <div class="secondary-metrics">
 85 |                 <div class="metric draft-metric">
 86 |                   <a href="https://github.com/search?q=is:pr+head:copilot/+is:draft&type=pullrequests" target="_blank">
 87 |                     <span class="metric-count">15,516</span>
 88 |                     <span>draft</span>
 89 |                   </a>
 90 |                 </div>
 91 |                 <div class="metric">
 92 |                   <a href="https://github.com/search?q=is:pr+head:copilot/+-is:draft&type=pullrequests" target="_blank">
 93 |                     <span class="metric-count">24,213</span>
 94 |                     <span>ready</span>
 95 |                   </a>
 96 |                 </div>
 97 |                 <div class="metric">
 98 |                   <a href="https://github.com/search?q=is:pr+head:copilot/+is:merged&type=pullrequests" target="_blank">
 99 |                     <span class="metric-count">22,200</span>
100 |                     <span>merged</span>
101 |                   </a>
102 |                 </div>
103 |               </div>
104 |             </div>
105 |           <div class="progress-bar">
106 |             <div
107 |               class="progress"
108 |               style="width: 91.68628422748111%; background-color: #2563eb"
109 |             ></div>
110 |           </div>
111 |         </div>
112 |         
113 |           <div class="agent"
114 |                data-success-rate="87.96801781599419"
115 |                data-success-rate-ready="87.96801781599419"
116 |                data-success-rate-total="87.02360255358211"
117 |                data-total-prs="718990"
118 |                data-ready-prs="711271"
119 |                data-merged-prs="625691"
120 |                data-agent-name="OpenAI Codex">
121 |             <div class="agent-header">
122 |               <div class="rank" data-rank="2">#2</div>
123 |               <div class="agent-dot" style="background-color: #dc2626"></div>
124 |               <div class="agent-info">
125 |                 <h3><a href="https://openai.com/index/introducing-codex/" target="_blank">OpenAI Codex</a></h3>
126 |                 <span class="agent-subtitle">
127 |                   <span class="pr-count">711,271</span>
128 |                   <span class="pr-type">ready</span> /
129 |                   <span class="pr-total">718,990</span>
130 |                   <span class="pr-total-label">all PRs</span>
131 |                 </span>
132 |               </div>
133 |             </div>
134 |             <div class="metrics">
135 |               <div class="primary-metric">
136 |                 <span class="metric-value">
137 |                   88.0%
138 |                 </span>
139 |                 <span class="metric-label">Success Rate</span>
140 |               </div>
141 |               <div class="secondary-metrics">
142 |                 <div class="metric draft-metric">
143 |                   <a href="https://github.com/search?q=is:pr+head:codex/+is:draft&type=pullrequests" target="_blank">
144 |                     <span class="metric-count">7,719</span>
145 |                     <span>draft</span>
146 |                   </a>
147 |                 </div>
148 |                 <div class="metric">
149 |                   <a href="https://github.com/search?q=is:pr+head:codex/+-is:draft&type=pullrequests" target="_blank">
150 |                     <span class="metric-count">711,271</span>
151 |                     <span>ready</span>
152 |                   </a>
153 |                 </div>
154 |                 <div class="metric">
155 |                   <a href="https://github.com/search?q=is:pr+head:codex/+is:merged&type=pullrequests" target="_blank">
156 |                     <span class="metric-count">625,691</span>
157 |                     <span>merged</span>
158 |                   </a>
159 |                 </div>
160 |               </div>
161 |             </div>
162 |           <div class="progress-bar">
163 |             <div
164 |               class="progress"
165 |               style="width: 87.96801781599419%; background-color: #dc2626"
166 |             ></div>
167 |           </div>
168 |         </div>
169 |         
170 |           <div class="agent"
171 |                data-success-rate="76.1633428300095"
172 |                data-success-rate-ready="76.1633428300095"
173 |                data-success-rate-total="75.46656908359036"
174 |                data-total-prs="19129"
175 |                data-ready-prs="18954"
176 |                data-merged-prs="14436"
177 |                data-agent-name="Cursor Agents">
178 |             <div class="agent-header">
179 |               <div class="rank" data-rank="3">#3</div>
180 |               <div class="agent-dot" style="background-color: #7c3aed"></div>
181 |               <div class="agent-info">
182 |                 <h3><a href="https://docs.cursor.com/background-agent" target="_blank">Cursor Agents</a></h3>
183 |                 <span class="agent-subtitle">
184 |                   <span class="pr-count">18,954</span>
185 |                   <span class="pr-type">ready</span> /
186 |                   <span class="pr-total">19,129</span>
187 |                   <span class="pr-total-label">all PRs</span>
188 |                 </span>
189 |               </div>
190 |             </div>
191 |             <div class="metrics">
192 |               <div class="primary-metric">
193 |                 <span class="metric-value">
194 |                   76.2%
195 |                 </span>
196 |                 <span class="metric-label">Success Rate</span>
197 |               </div>
198 |               <div class="secondary-metrics">
199 |                 <div class="metric draft-metric">
200 |                   <a href="https://github.com/search?q=is:pr+head:cursor/+is:draft&type=pullrequests" target="_blank">
201 |                     <span class="metric-count">175</span>
202 |                     <span>draft</span>
203 |                   </a>
204 |                 </div>
205 |                 <div class="metric">
206 |                   <a href="https://github.com/search?q=is:pr+head:cursor/+-is:draft&type=pullrequests" target="_blank">
207 |                     <span class="metric-count">18,954</span>
208 |                     <span>ready</span>
209 |                   </a>
210 |                 </div>
211 |                 <div class="metric">
212 |                   <a href="https://github.com/search?q=is:pr+head:cursor/+is:merged&type=pullrequests" target="_blank">
213 |                     <span class="metric-count">14,436</span>
214 |                     <span>merged</span>
215 |                   </a>
216 |                 </div>
217 |               </div>
218 |             </div>
219 |           <div class="progress-bar">
220 |             <div
221 |               class="progress"
222 |               style="width: 76.1633428300095%; background-color: #7c3aed"
223 |             ></div>
224 |           </div>
225 |         </div>
226 |         
227 |           <div class="agent"
228 |                data-success-rate="65.80504412785508"
229 |                data-success-rate-ready="65.80504412785508"
230 |                data-success-rate-total="63.78520393451027"
231 |                data-total-prs="31211"
232 |                data-ready-prs="30253"
233 |                data-merged-prs="19908"
234 |                data-agent-name="Devin">
235 |             <div class="agent-header">
236 |               <div class="rank" data-rank="4">#4</div>
237 |               <div class="agent-dot" style="background-color: #059669"></div>
238 |               <div class="agent-info">
239 |                 <h3><a href="https://devin.ai/pricing" target="_blank">Devin</a></h3>
240 |                 <span class="agent-subtitle">
241 |                   <span class="pr-count">30,253</span>
242 |                   <span class="pr-type">ready</span> /
243 |                   <span class="pr-total">31,211</span>
244 |                   <span class="pr-total-label">all PRs</span>
245 |                 </span>
246 |               </div>
247 |             </div>
248 |             <div class="metrics">
249 |               <div class="primary-metric">
250 |                 <span class="metric-value">
251 |                   65.8%
252 |                 </span>
253 |                 <span class="metric-label">Success Rate</span>
254 |               </div>
255 |               <div class="secondary-metrics">
256 |                 <div class="metric draft-metric">
257 |                   <a href="https://github.com/search?q=is:pr+author:devin-ai-integration[bot]+is:draft&type=pullrequests" target="_blank">
258 |                     <span class="metric-count">958</span>
259 |                     <span>draft</span>
260 |                   </a>
261 |                 </div>
262 |                 <div class="metric">
263 |                   <a href="https://github.com/search?q=is:pr+author:devin-ai-integration[bot]+-is:draft&type=pullrequests" target="_blank">
264 |                     <span class="metric-count">30,253</span>
265 |                     <span>ready</span>
266 |                   </a>
267 |                 </div>
268 |                 <div class="metric">
269 |                   <a href="https://github.com/search?q=is:pr+author:devin-ai-integration[bot]+is:merged&type=pullrequests" target="_blank">
270 |                     <span class="metric-count">19,908</span>
271 |                     <span>merged</span>
272 |                   </a>
273 |                 </div>
274 |               </div>
275 |             </div>
276 |           <div class="progress-bar">
277 |             <div
278 |               class="progress"
279 |               style="width: 65.80504412785508%; background-color: #059669"
280 |             ></div>
281 |           </div>
282 |         </div>
283 |         
284 |           <div class="agent"
285 |                data-success-rate="77.11899791231733"
286 |                data-success-rate-ready="77.11899791231733"
287 |                data-success-rate-total="41.393993724787094"
288 |                data-total-prs="4462"
289 |                data-ready-prs="2395"
290 |                data-merged-prs="1847"
291 |                data-agent-name="Codegen">
292 |             <div class="agent-header">
293 |               <div class="rank" data-rank="5">#5</div>
294 |               <div class="agent-dot" style="background-color: #d97706"></div>
295 |               <div class="agent-info">
296 |                 <h3><a href="https://codegen.com/" target="_blank">Codegen</a></h3>
297 |                 <span class="agent-subtitle">
298 |                   <span class="pr-count">2,395</span>
299 |                   <span class="pr-type">ready</span> /
300 |                   <span class="pr-total">4,462</span>
301 |                   <span class="pr-total-label">all PRs</span>
302 |                 </span>
303 |               </div>
304 |             </div>
305 |             <div class="metrics">
306 |               <div class="primary-metric">
307 |                 <span class="metric-value">
308 |                   77.1%
309 |                 </span>
310 |                 <span class="metric-label">Success Rate</span>
311 |               </div>
312 |               <div class="secondary-metrics">
313 |                 <div class="metric draft-metric">
314 |                   <a href="https://github.com/search?q=is:pr+author:codegen-sh[bot]+is:draft&type=pullrequests" target="_blank">
315 |                     <span class="metric-count">2,067</span>
316 |                     <span>draft</span>
317 |                   </a>
318 |                 </div>
319 |                 <div class="metric">
320 |                   <a href="https://github.com/search?q=is:pr+author:codegen-sh[bot]+-is:draft&type=pullrequests" target="_blank">
321 |                     <span class="metric-count">2,395</span>
322 |                     <span>ready</span>
323 |                   </a>
324 |                 </div>
325 |                 <div class="metric">
326 |                   <a href="https://github.com/search?q=is:pr+author:codegen-sh[bot]+is:merged&type=pullrequests" target="_blank">
327 |                     <span class="metric-count">1,847</span>
328 |                     <span>merged</span>
329 |                   </a>
330 |                 </div>
331 |               </div>
332 |             </div>
333 |           <div class="progress-bar">
334 |             <div
335 |               class="progress"
336 |               style="width: 77.11899791231733%; background-color: #d97706"
337 |             ></div>
338 |           </div>
339 |         </div>
340 |         
341 |       </section>
342 | 
343 |       <!-- Definitions section -->
344 |       <section class="definitions">
345 |         <div class="explanation-section">
346 |           <button class="explanation-toggle" id="explanationToggle">
347 |             <span class="toggle-text">IMPORTANT DEFINITIONS</span>
348 |             <span class="toggle-arrow">▼</span>
349 |           </button>
350 |           <div class="explanation-content" id="explanationContent">
351 |             <p>Different AI coding agents follow different workflows when creating pull requests:</p>
352 |             <ul>
353 |               <li><strong>All PRs:</strong> Every pull request created by an agent, including DRAFT PRs.</li>
354 |               <li><strong>Ready PRs:</strong> Non-draft pull requests that are ready for review and merging</li>
355 |               <li><strong>Merged PRs:</strong> Pull requests that were successfully merged into the codebase</li>
356 |             </ul>
357 |             <p><strong>Key workflow differences:</strong> Some agents like <strong>Codex</strong> iterate privately and create ready PRs directly, resulting in very few drafts but high merge rates. Others like <strong>Copilot</strong> and <strong>Codegen</strong> create draft PRs first, encouraging public iteration before marking them ready for review.</p>
358 |             <p>By default, we show success rates using <strong>Ready PRs only</strong> to fairly compare agents across different workflows. This focuses on each agent's ability to produce mergeable code, regardless of whether they iterate publicly (with drafts) or privately. Toggle to "Include draft PRs" to see the complete picture of all activity.</p>
359 |           </div>
360 |         </div>
361 |       </section>
362 | 
363 |       <!-- Historic Chart -->
364 |       <section class="chart">
365 |         <h2>PR Volume & Success Rate</h2>
366 |         <div class="chart-controls">
367 |           <div class="control-row">
368 |             <div class="control-section">
369 |               <span class="control-label">AGENTS</span>
370 |               <div class="toggle-buttons">
371 |                 
372 |                 <button
373 |                   id="toggleCopilot"
374 |                   class="toggle-btn active"
375 |                   data-agent="copilot"
376 |                 >
377 |                   <span
378 |                     class="toggle-icon"
379 |                     style="background-color: #2563eb"
380 |                   ></span
381 |                   >GitHub Copilot
382 |                 </button>
383 |                 
384 |                 <button
385 |                   id="toggleCodex"
386 |                   class="toggle-btn active"
387 |                   data-agent="codex"
388 |                 >
389 |                   <span
390 |                     class="toggle-icon"
391 |                     style="background-color: #dc2626"
392 |                   ></span
393 |                   >OpenAI Codex
394 |                 </button>
395 |                 
396 |                 <button
397 |                   id="toggleCursor"
398 |                   class="toggle-btn active"
399 |                   data-agent="cursor"
400 |                 >
401 |                   <span
402 |                     class="toggle-icon"
403 |                     style="background-color: #7c3aed"
404 |                   ></span
405 |                   >Cursor Agents
406 |                 </button>
407 |                 
408 |                 <button
409 |                   id="toggleDevin"
410 |                   class="toggle-btn active"
411 |                   data-agent="devin"
412 |                 >
413 |                   <span
414 |                     class="toggle-icon"
415 |                     style="background-color: #059669"
416 |                   ></span
417 |                   >Devin
418 |                 </button>
419 |                 
420 |                 <button
421 |                   id="toggleCodegen"
422 |                   class="toggle-btn active"
423 |                   data-agent="codegen"
424 |                 >
425 |                   <span
426 |                     class="toggle-icon"
427 |                     style="background-color: #d97706"
428 |                   ></span
429 |                   >Codegen
430 |                 </button>
431 |                 
432 |               </div>
433 |             </div>
434 |             <div class="control-section">
435 |               <span class="control-label">SUCCESS RATE</span>
436 |               <div class="toggle-buttons">
437 |                 <button id="rateReady" class="rate-btn active" data-rate="ready">
438 |                   <span class="rate-icon">●</span>Ready PRs Only (M/R)
439 |                 </button>
440 |                 <button id="rateTotal" class="rate-btn" data-rate="total">
441 |                   All PRs (M/A)
442 |                 </button>
443 |               </div>
444 |             </div>
445 |             <div class="control-section">
446 |               <span class="control-label">VIEW MODE</span>
447 |               <div class="toggle-buttons">
448 |                 <button id="viewAll" class="view-btn active" data-view="all">
449 |                   <span class="view-icon">●</span>Complete View
450 |                 </button>
451 |                 <button id="viewBarsOnly" class="view-btn" data-view="bars">
452 |                   Volume Only
453 |                 </button>
454 |                 <button id="viewLinesOnly" class="view-btn" data-view="lines">
455 |                   Success Rate Only
456 |                 </button>
457 |               </div>
458 |             </div>
459 |           </div>
460 |         </div>
461 |         <div class="chart-container">
462 |           <canvas id="prChart" width="800" height="400"></canvas>
463 |         </div>
464 |       </section>
465 | 
466 |       <!-- Simple Footer -->
467 |       <footer class="footer">
468 |         <p>
469 |           Updated July 16, 2025 09:03 UTC •
470 |           <a href="https://github.com/aavetis/ai-pr-watcher" target="_blank">by aavetis</a>
471 |         </p>
472 |       </footer>
473 |     </div>
474 | 
475 |     <script>
476 |       // Chart instance
477 |       let chartInstance = null;
478 |       let currentRateType = 'ready'; // Track current success rate type
479 |       let currentChartRateType = 'ready'; // Track current chart success rate type
480 | 
481 |       document.addEventListener("DOMContentLoaded", function () {
482 |         initializeSorting();
483 |         initializeDropdown();
484 |         initializeExplanation();
485 |         loadCharts();
486 |         // Sort by total PRs on page load
487 |         sortAgents("total-prs");
488 |       });
489 | 
490 |       // Dynamic Sorting
491 |       function initializeSorting() {
492 |         const sortButtons = document.querySelectorAll(".sort-btn");
493 |         sortButtons.forEach((btn) => {
494 |           if (!btn.classList.contains('dropdown-btn')) {
495 |             btn.addEventListener("click", () => {
496 |               // Update active button
497 |               sortButtons.forEach((b) => b.classList.remove("active"));
498 |               btn.classList.add("active");
499 | 
500 |               // Sort agents
501 |               sortAgents(btn.dataset.sort);
502 |             });
503 |           }
504 |         });
505 |       }
506 | 
507 |       function initializeDropdown() {
508 |         const dropdownBtn = document.getElementById('successRateBtn');
509 |         const dropdown = document.getElementById('successRateDropdown');
510 |         const dropdownItems = document.querySelectorAll('.dropdown-item');
511 |         const arrow = dropdownBtn.querySelector('.dropdown-arrow');
512 | 
513 |         // Toggle dropdown
514 |         dropdownBtn.addEventListener('click', (e) => {
515 |           e.stopPropagation();
516 |           const isShowing = dropdown.classList.contains('show');
517 |           dropdown.classList.toggle('show');
518 | 
519 |           // Rotate arrow
520 |           if (dropdown.classList.contains('show')) {
521 |             arrow.style.transform = 'rotate(180deg)';
522 |           } else {
523 |             arrow.style.transform = 'rotate(0deg)';
524 |           }
525 |         });
526 | 
527 |         // Close dropdown when clicking outside
528 |         document.addEventListener('click', () => {
529 |           dropdown.classList.remove('show');
530 |           arrow.style.transform = 'rotate(0deg)';
531 |         });
532 | 
533 |         // Handle dropdown item selection
534 |         dropdownItems.forEach(item => {
535 |           item.addEventListener('click', (e) => {
536 |             e.stopPropagation();
537 | 
538 |             // Update active item
539 |             dropdownItems.forEach(i => i.classList.remove('active'));
540 |             item.classList.add('active');
541 | 
542 |             // Update rate type
543 |             currentRateType = item.dataset.rateType;
544 | 
545 |             // Update button text to show current mode
546 |             const modeText = currentRateType === 'ready' ? '(M/R)' : '(M/A)';
547 |             dropdownBtn.innerHTML = `Success Rate <span class="mode-indicator">${modeText}</span> <span class="dropdown-arrow">▼</span>`;
548 | 
549 |             // Update all agents' data and resort
550 |             updateAgentMetrics(currentRateType);
551 |             sortAgents('success-rate');
552 | 
553 |             // Close dropdown
554 |             dropdown.classList.remove('show');
555 |             arrow.style.transform = 'rotate(0deg)';
556 | 
557 |             // Make sure success rate button is active
558 |             document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
559 |             dropdownBtn.classList.add('active');
560 |           });
561 |         });
562 |       }
563 | 
564 |       function initializeExplanation() {
565 |         const toggle = document.getElementById('explanationToggle');
566 |         const content = document.getElementById('explanationContent');
567 |         const arrow = toggle.querySelector('.toggle-arrow');
568 | 
569 |         toggle.addEventListener('click', () => {
570 |           const isExpanded = content.classList.contains('expanded');
571 | 
572 |           if (isExpanded) {
573 |             content.classList.remove('expanded');
574 |             toggle.classList.remove('expanded');
575 |           } else {
576 |             content.classList.add('expanded');
577 |             toggle.classList.add('expanded');
578 |           }
579 |         });
580 |       }
581 | 
582 |       function updateAgentMetrics(rateType) {
583 |         const agents = document.querySelectorAll('.agent');
584 | 
585 |         agents.forEach(agent => {
586 |           const rate = rateType === 'ready'
587 |             ? parseFloat(agent.dataset.successRateReady)
588 |             : parseFloat(agent.dataset.successRateTotal);
589 | 
590 |           // Update the data attribute for sorting
591 |           agent.dataset.successRate = rate;
592 | 
593 |           // Update the primary metric display
594 |           const metricValue = agent.querySelector('.metric-value');
595 |           metricValue.textContent = rate.toFixed(1) + '%';
596 | 
597 |           // Update the progress bar
598 |           const progressBar = agent.querySelector('.progress');
599 |           progressBar.style.width = rate + '%';
600 | 
601 |           // Update subtitle to highlight the current mode
602 |           const subtitle = agent.querySelector('.agent-subtitle');
603 |           const prCount = subtitle.querySelector('.pr-count');
604 |           const prType = subtitle.querySelector('.pr-type');
605 | 
606 |           if (rateType === 'ready') {
607 |             prCount.textContent = parseInt(agent.dataset.readyPrs).toLocaleString();
608 |             prType.textContent = 'ready';
609 |             prType.style.fontWeight = '600';
610 |             subtitle.querySelector('.pr-total-label').style.opacity = '0.6';
611 |           } else {
612 |             prCount.textContent = parseInt(agent.dataset.totalPrs).toLocaleString();
613 |             prType.textContent = 'all';
614 |             prType.style.fontWeight = '600';
615 |             subtitle.querySelector('.pr-total-label').style.opacity = '1';
616 |           }
617 |         });
618 |       }
619 | 
620 |       function sortAgents(criteria) {
621 |         const container = document.getElementById("agentsContainer");
622 |         const agents = Array.from(container.children);
623 | 
624 |         agents.sort((a, b) => {
625 |           let valueA, valueB;
626 | 
627 |           switch (criteria) {
628 |             case "success-rate":
629 |               valueA = parseFloat(a.dataset.successRate);
630 |               valueB = parseFloat(b.dataset.successRate);
631 |               break;
632 |             case "total-prs":
633 |               valueA = parseInt(a.dataset.totalPrs);
634 |               valueB = parseInt(b.dataset.totalPrs);
635 |               break;
636 |             case "merged-prs":
637 |               valueA = parseInt(a.dataset.mergedPrs);
638 |               valueB = parseInt(b.dataset.mergedPrs);
639 |               break;
640 |             default:
641 |               return 0;
642 |           }
643 | 
644 |           return valueB - valueA; // Descending order
645 |         });
646 | 
647 |         // Update ranks and re-append in new order
648 |         agents.forEach((agent, index) => {
649 |           const rank = agent.querySelector(".rank");
650 |           rank.textContent = `#${index + 1}`;
651 |           rank.dataset.rank = index + 1;
652 |           container.appendChild(agent);
653 |         });
654 | 
655 |         // Add animation
656 |         agents.forEach((agent, index) => {
657 |           agent.style.animation = "none";
658 |           setTimeout(() => {
659 |             agent.style.animation = `fadeInUp 0.4s ease forwards`;
660 |             agent.style.animationDelay = `${index * 0.1}s`;
661 |           }, 10);
662 |         });
663 |       }
664 | 
665 |       // Chart Loading
666 |       async function loadCharts() {
667 |         try {
668 |           console.log("Loading chart data...");
669 |           const response = await fetch("chart-data.json");
670 |           if (!response.ok) {
671 |             throw new Error(`HTTP error! status: ${response.status}`);
672 |           }
673 |           const data = await response.json();
674 |           console.log("Chart data loaded:", data);
675 | 
676 |           initializeChart(data);
677 |         } catch (error) {
678 |           console.error("Charts failed to load:", error);
679 |           // Show error message in chart
680 |           document.querySelector(".chart-container").innerHTML =
681 |             '<p style="color: #ef4444; text-align: center; padding: 2rem;">Failed to load chart</p>';
682 |         }
683 |       }
684 | 
685 |       function initializeChart(chartData) {
686 |         console.log("Initializing combined chart...");
687 |         const ctx = document.getElementById("prChart").getContext("2d");
688 | 
689 |         chartInstance = new Chart(ctx, {
690 |           type: "bar",
691 |           data: chartData,
692 |           options: {
693 |             responsive: true,
694 |             maintainAspectRatio: false,
695 |             interaction: {
696 |               intersect: false,
697 |               mode: "point",
698 |             },
699 |             plugins: {
700 |               title: {
701 |                 display: false,
702 |               },
703 |               legend: {
704 |                 display: true,
705 |                 position: "right",
706 |                 labels: {
707 |                   usePointStyle: true,
708 |                   pointStyle: "circle",
709 |                   color: "#64748b",
710 |                   font: {
711 |                     family: "IBM Plex Mono",
712 |                     size: 11,
713 |                     weight: "500",
714 |                   },
715 |                   padding: 15,
716 |                   boxWidth: 12,
717 |                 },
718 |               },
719 |               tooltip: {
720 |                 titleColor: "#1f2937",
721 |                 bodyColor: "#374151",
722 |                 backgroundColor: "rgba(255, 255, 255, 0.95)",
723 |                 borderColor: "#e5e7eb",
724 |                 borderWidth: 1,
725 |                 callbacks: {
726 |                   title: function (context) {
727 |                     return context[0].label;
728 |                   },
729 |                   label: function (context) {
730 |                     const datasetLabel = context.dataset.label;
731 |                     const value = context.parsed.y;
732 | 
733 |                     if (datasetLabel.includes("Success %")) {
734 |                       return datasetLabel + ": " + value.toFixed(1) + "%";
735 |                     } else {
736 |                       return (
737 |                         datasetLabel + ": " + value.toLocaleString() + " PRs"
738 |                       );
739 |                     }
740 |                   },
741 |                 },
742 |               },
743 |             },
744 |             scales: {
745 |               x: {
746 |                 title: {
747 |                   display: true,
748 |                   text: "Time",
749 |                   color: "#475569",
750 |                   font: {
751 |                     family: "IBM Plex Mono",
752 |                     size: 12,
753 |                     weight: "500",
754 |                   },
755 |                 },
756 |                 grid: { color: "#f1f5f9", lineWidth: 1 },
757 |                 ticks: {
758 |                   color: "#475569",
759 |                   font: { family: "IBM Plex Mono", size: 11 },
760 |                   maxRotation: 45,
761 |                   minRotation: 0
762 |                 },
763 |               },
764 |               y: {
765 |                 type: "linear",
766 |                 display: true,
767 |                 position: "left",
768 |                 title: {
769 |                   display: true,
770 |                   text: "Number of PRs",
771 |                   color: "#475569",
772 |                   font: {
773 |                     family: "IBM Plex Mono",
774 |                     size: 12,
775 |                     weight: "500",
776 |                   },
777 |                 },
778 |                 grid: { color: "#f1f5f9", lineWidth: 1 },
779 |                 ticks: {
780 |                   color: "#475569",
781 |                   font: { family: "IBM Plex Mono", size: 11 },
782 |                   callback: function(value) {
783 |                     return value.toLocaleString();
784 |                   }
785 |                 },
786 |                 beginAtZero: true,
787 |               },
788 |               y1: {
789 |                 type: "linear",
790 |                 display: true,
791 |                 position: "right",
792 |                 title: {
793 |                   display: true,
794 |                   text: "Success Rate (%)",
795 |                   color: "#475569",
796 |                   font: {
797 |                     family: "IBM Plex Mono",
798 |                     size: 12,
799 |                     weight: "500",
800 |                   },
801 |                 },
802 |                 ticks: {
803 |                   color: "#475569",
804 |                   font: { family: "IBM Plex Mono", size: 11 },
805 |                   callback: function (value) {
806 |                     return value + "%";
807 |                   },
808 |                 },
809 |                 beginAtZero: true,
810 |                 max: 100,
811 |                 grid: {
812 |                   drawOnChartArea: false,
813 |                 },
814 |               },
815 |             },
816 |           },
817 |         });
818 | 
819 |         console.log("Chart initialized");
820 | 
821 |         // Set up toggle functionality
822 |         setupToggleButtons();
823 | 
824 |         // Initialize chart with proper dataset visibility
825 |         updateChartVisibility();
826 |       }
827 | 
828 |       function updateChartVisibility() {
829 |         if (!chartInstance) return;
830 | 
831 |         chartInstance.data.datasets.forEach((dataset) => {
832 |           const isLine = dataset.type === "line";
833 |           const isBar = dataset.type === "bar";
834 |           const labelLower = dataset.label.toLowerCase();
835 | 
836 |           // Get the agent name from the dataset label
837 |           const agentMatch = labelLower.match(/(copilot|codex|cursor|devin|codegen)/);
838 |           const agent = agentMatch ? agentMatch[1] : null;
839 | 
840 |           // Check if agent is currently enabled
841 |           const agentButton = document.querySelector(`.toggle-btn[data-agent="${agent}"]`);
842 |           const agentEnabled = agentButton && agentButton.classList.contains("active");
843 | 
844 |           // Check current view mode
845 |           const activeViewBtn = document.querySelector(".view-btn.active");
846 |           const currentView = activeViewBtn ? activeViewBtn.dataset.view : "all";
847 | 
848 |           // For lines, check rate type
849 |           const isCorrectRateType = isLine ?
850 |             (currentChartRateType === 'ready' ? labelLower.includes('ready') : labelLower.includes('all')) :
851 |             true;
852 | 
853 |           // Set visibility based on all conditions
854 |           switch (currentView) {
855 |             case "all":
856 |               dataset.hidden = !agentEnabled || (isLine && !isCorrectRateType);
857 |               break;
858 |             case "bars":
859 |               dataset.hidden = !agentEnabled || isLine;
860 |               break;
861 |             case "lines":
862 |               dataset.hidden = !agentEnabled || isBar || (isLine && !isCorrectRateType);
863 |               break;
864 |           }
865 |         });
866 | 
867 |         chartInstance.update();
868 |       }
869 | 
870 |       function setupToggleButtons() {
871 |         const toggleButtons = document.querySelectorAll(".toggle-btn");
872 |         const viewButtons = document.querySelectorAll(".view-btn");
873 |         const rateButtons = document.querySelectorAll(".rate-btn");
874 | 
875 |         // Agent toggle functionality
876 |         toggleButtons.forEach((button) => {
877 |           button.addEventListener("click", function () {
878 |             const agent = this.dataset.agent;
879 | 
880 |             // Toggle button state
881 |             this.classList.toggle("active");
882 | 
883 |             // Update chart visibility
884 |             updateChartVisibility();
885 |           });
886 |         });
887 | 
888 |         // Success rate type functionality
889 |         rateButtons.forEach((button) => {
890 |           button.addEventListener("click", function () {
891 |             const rateType = this.dataset.rate;
892 | 
893 |             // Update button states
894 |             rateButtons.forEach((btn) => btn.classList.remove("active"));
895 |             this.classList.add("active");
896 | 
897 |             // Update current rate type
898 |             currentChartRateType = rateType;
899 | 
900 |             // Update chart visibility
901 |             updateChartVisibility();
902 |           });
903 |         });
904 | 
905 |         // View mode functionality
906 |         viewButtons.forEach((button) => {
907 |           button.addEventListener("click", function () {
908 |             const view = this.dataset.view;
909 | 
910 |             // Update button states
911 |             viewButtons.forEach((btn) => btn.classList.remove("active"));
912 |             this.classList.add("active");
913 | 
914 |             // Update chart visibility
915 |             updateChartVisibility();
916 |           });
917 |         });
918 |       }
919 |     </script>
920 |   </body>
921 | </html>


--------------------------------------------------------------------------------
/docs/styles.css:
--------------------------------------------------------------------------------
   1 | /* Elegant Professional CSS Reset */
   2 | :root {
   3 |   /* Color System */
   4 |   --color-primary: #1a202c;
   5 |   --color-secondary: #64748b;
   6 |   --color-accent: #7c5fa3;
   7 |   --color-accent-hover: #6b4789;
   8 |   --color-success: #38a169;
   9 |   --color-success-hover: #2f855a;
  10 |   --color-background: #faf9f7;
  11 |   --color-surface: #fcfbf9;
  12 |   --color-border: #e8e6e1;
  13 |   --color-border-hover: #d4d1ca;
  14 | 
  15 |   /* Typography */
  16 |   --font-serif: "Playfair Display", Georgia, serif;
  17 |   --font-mono: "IBM Plex Mono", "SF Mono", Monaco, monospace;
  18 | }
  19 | 
  20 | *,
  21 | *::before,
  22 | *::after {
  23 |   margin: 0;
  24 |   padding: 0;
  25 |   box-sizing: border-box;
  26 | }
  27 | 
  28 | /* Typography optimization */
  29 | h1,
  30 | h2,
  31 | h3,
  32 | h4,
  33 | h5,
  34 | h6 {
  35 |   font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
  36 |   text-rendering: optimizeLegibility;
  37 | }
  38 | 
  39 | /* Monospace number optimization */
  40 | .metric-value,
  41 | .rank,
  42 | .metric a {
  43 |   font-feature-settings: "kern" 1, "tnum" 1, "case" 1;
  44 |   font-variant-numeric: tabular-nums;
  45 | }
  46 | 
  47 | body {
  48 |   font-family: var(--font-mono);
  49 |   line-height: 1.6;
  50 |   background: var(--color-background);
  51 |   background-image: radial-gradient(
  52 |       circle at 20% 80%,
  53 |       rgba(120, 119, 108, 0.03) 0%,
  54 |       transparent 50%
  55 |     ),
  56 |     radial-gradient(
  57 |       circle at 80% 20%,
  58 |       rgba(120, 119, 108, 0.03) 0%,
  59 |       transparent 50%
  60 |     ),
  61 |     radial-gradient(
  62 |       circle at 40% 40%,
  63 |       rgba(120, 119, 108, 0.02) 0%,
  64 |       transparent 50%
  65 |     );
  66 |   color: var(--color-primary);
  67 |   padding: 0;
  68 |   min-height: 100vh;
  69 |   font-feature-settings: "kern" 1, "liga" 1, "frac" 1, "calt" 1;
  70 | }
  71 | 
  72 | .container {
  73 |   max-width: 1200px;
  74 |   margin: 0 auto;
  75 |   padding: 2.5rem 2rem;
  76 | }
  77 | 
  78 | /* Hero Section */
  79 | .hero {
  80 |   text-align: left;
  81 |   margin-bottom: 3rem;
  82 |   padding: 0;
  83 |   background: none;
  84 |   border-bottom: 1px solid var(--color-border);
  85 |   padding-bottom: 2rem;
  86 | }
  87 | 
  88 | .hero h1 {
  89 |   font-family: var(--font-serif);
  90 |   font-size: 2.75rem;
  91 |   font-weight: 600;
  92 |   color: var(--color-primary);
  93 |   margin-bottom: 0.5rem;
  94 |   letter-spacing: -0.02em;
  95 |   line-height: 1.1;
  96 | }
  97 | 
  98 | .hero p {
  99 |   font-family: var(--font-mono);
 100 |   font-size: 1rem;
 101 |   color: var(--color-secondary);
 102 |   font-weight: 400;
 103 |   max-width: 28rem;
 104 |   margin: 0;
 105 |   letter-spacing: 0.01em;
 106 |   line-height: 1.5;
 107 | }
 108 | 
 109 | /* Leaderboard */
 110 | .leaderboard {
 111 |   margin-bottom: 3rem;
 112 | }
 113 | 
 114 | .leaderboard-header {
 115 |   display: flex;
 116 |   justify-content: space-between;
 117 |   align-items: flex-end;
 118 |   margin-bottom: 1.5rem;
 119 |   flex-wrap: wrap;
 120 |   gap: 1rem;
 121 | }
 122 | 
 123 | .leaderboard h2 {
 124 |   font-family: "Playfair Display", Georgia, serif;
 125 |   font-size: 2rem;
 126 |   font-weight: 600;
 127 |   color: #1a202c;
 128 |   margin: 0;
 129 |   letter-spacing: -0.02em;
 130 |   line-height: 1.2;
 131 | }
 132 | 
 133 | /* Leaderboard Container */
 134 | .agents-container {
 135 |   background: var(--color-surface);
 136 |   border: 1px solid var(--color-border);
 137 |   border-radius: 8px;
 138 |   overflow: hidden;
 139 |   box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
 140 | }
 141 | 
 142 | /* Unified Button System */
 143 | .btn {
 144 |   font-family: "IBM Plex Mono", "SF Mono", Monaco, monospace;
 145 |   display: inline-flex;
 146 |   align-items: center;
 147 |   gap: 0.5rem;
 148 |   padding: 0.625rem 1rem;
 149 |   border: 1px solid var(--color-border);
 150 |   border-radius: 6px;
 151 |   background: var(--color-surface);
 152 |   color: #5a6674;
 153 |   font-size: 0.75rem;
 154 |   font-weight: 500;
 155 |   text-decoration: none;
 156 |   cursor: pointer;
 157 |   transition: all 0.2s ease;
 158 |   white-space: nowrap;
 159 |   user-select: none;
 160 | }
 161 | 
 162 | .btn:hover {
 163 |   border-color: var(--color-border-hover);
 164 |   color: #475569;
 165 |   background: #f6f4f1;
 166 |   box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
 167 | }
 168 | 
 169 | .btn.active {
 170 |   background: var(--color-accent);
 171 |   border-color: var(--color-accent);
 172 |   color: white;
 173 |   box-shadow: 0 1px 3px 0 rgba(124, 95, 163, 0.25);
 174 | }
 175 | 
 176 | .btn.active:hover {
 177 |   background: var(--color-accent-hover);
 178 |   border-color: var(--color-accent-hover);
 179 | }
 180 | 
 181 | /* Button variants */
 182 | .btn--sm {
 183 |   padding: 0.5rem 0.75rem;
 184 |   font-size: 0.6875rem;
 185 | }
 186 | 
 187 | .btn--xs {
 188 |   padding: 0.375rem 0.625rem;
 189 |   font-size: 0.625rem;
 190 | }
 191 | 
 192 | .btn--primary.active {
 193 |   background: #3b82f6;
 194 |   border-color: #3b82f6;
 195 | }
 196 | 
 197 | .btn--primary.active:hover {
 198 |   background: #2563eb;
 199 |   border-color: #2563eb;
 200 | }
 201 | 
 202 | .btn--success.active {
 203 |   background: #10b981;
 204 |   border-color: #10b981;
 205 | }
 206 | 
 207 | .btn--success.active:hover {
 208 |   background: #059669;
 209 |   border-color: #059669;
 210 | }
 211 | 
 212 | /* Button icon styling */
 213 | .btn-icon {
 214 |   width: 8px;
 215 |   height: 8px;
 216 |   border-radius: 50%;
 217 |   display: inline-block;
 218 |   flex-shrink: 0;
 219 | }
 220 | 
 221 | .sort-controls {
 222 |   display: flex;
 223 |   gap: 0.375rem;
 224 |   background: #f5f3f0;
 225 |   padding: 0.375rem;
 226 |   border-radius: 6px;
 227 |   border: 1px solid var(--color-border);
 228 | }
 229 | 
 230 | .sort-btn {
 231 |   font-family: "IBM Plex Mono", "SF Mono", Monaco, monospace;
 232 |   padding: 0.5rem 0.875rem;
 233 |   background: transparent;
 234 |   border: 1px solid transparent;
 235 |   border-radius: 6px;
 236 |   color: #5a6674;
 237 |   font-size: 0.6875rem;
 238 |   font-weight: 500;
 239 |   cursor: pointer;
 240 |   transition: all 0.2s ease;
 241 |   white-space: nowrap;
 242 |   text-transform: uppercase;
 243 |   letter-spacing: 0.08em;
 244 | }
 245 | 
 246 | .sort-btn:hover {
 247 |   background: var(--color-surface);
 248 |   border-color: var(--color-border);
 249 |   color: #475569;
 250 |   box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
 251 | }
 252 | 
 253 | .sort-btn.active {
 254 |   background: var(--color-accent);
 255 |   border-color: var(--color-accent);
 256 |   color: white;
 257 |   box-shadow: 0 1px 3px 0 rgba(124, 95, 163, 0.25);
 258 | }
 259 | 
 260 | /* Dropdown styles */
 261 | .dropdown-container {
 262 |   position: relative;
 263 | }
 264 | 
 265 | .dropdown-btn {
 266 |   position: relative;
 267 |   display: flex;
 268 |   align-items: center;
 269 |   gap: 0.5rem;
 270 | }
 271 | 
 272 | .dropdown-arrow {
 273 |   font-size: 0.5rem;
 274 |   transition: transform 0.2s ease;
 275 | }
 276 | 
 277 | .mode-indicator {
 278 |   font-weight: 400;
 279 |   opacity: 0.7;
 280 |   font-size: 0.625rem;
 281 | }
 282 | 
 283 | .dropdown-menu {
 284 |   position: absolute;
 285 |   top: 100%;
 286 |   left: 0;
 287 |   right: 0;
 288 |   background: var(--color-surface);
 289 |   border: 1px solid var(--color-border);
 290 |   border-radius: 6px;
 291 |   box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
 292 |     0 2px 4px -1px rgba(0, 0, 0, 0.06);
 293 |   z-index: 50;
 294 |   opacity: 0;
 295 |   visibility: hidden;
 296 |   transform: translateY(-4px);
 297 |   transition: all 0.15s ease;
 298 |   min-width: 160px;
 299 | }
 300 | 
 301 | .dropdown-menu.show {
 302 |   opacity: 1;
 303 |   visibility: visible;
 304 |   transform: translateY(0);
 305 | }
 306 | 
 307 | .dropdown-item {
 308 |   display: block;
 309 |   width: 100%;
 310 |   padding: 0.5rem 0.75rem;
 311 |   background: transparent;
 312 |   border: none;
 313 |   text-align: left;
 314 |   font-family: "IBM Plex Mono", "SF Mono", Monaco, monospace;
 315 |   font-size: 0.6875rem;
 316 |   font-weight: 500;
 317 |   color: #374151;
 318 |   cursor: pointer;
 319 |   transition: all 0.15s ease;
 320 |   text-transform: none;
 321 |   letter-spacing: 0;
 322 |   border-radius: 0;
 323 | }
 324 | 
 325 | .dropdown-item:first-child {
 326 |   border-radius: 5px 5px 0 0;
 327 | }
 328 | 
 329 | .dropdown-item:last-child {
 330 |   border-radius: 0 0 5px 5px;
 331 | }
 332 | 
 333 | .dropdown-item:hover {
 334 |   background: #f6f4f1;
 335 |   color: #1f2937;
 336 | }
 337 | 
 338 | .dropdown-item.active {
 339 |   background: #f3f0f8;
 340 |   color: var(--color-accent);
 341 |   font-weight: 600;
 342 | }
 343 | 
 344 | /* Enhanced agent subtitle styling */
 345 | .agent-subtitle {
 346 |   font-size: 0.6875rem;
 347 |   color: #64748b;
 348 |   font-weight: 400;
 349 | }
 350 | 
 351 | .agent-subtitle .pr-count {
 352 |   font-weight: 600;
 353 |   color: #374151;
 354 | }
 355 | 
 356 | .agent-subtitle .pr-type {
 357 |   font-weight: 600;
 358 |   color: var(--color-accent);
 359 | }
 360 | 
 361 | .agent-subtitle .pr-total {
 362 |   color: #9ca3af;
 363 | }
 364 | 
 365 | .agent-subtitle .pr-total-label {
 366 |   color: #9ca3af;
 367 | }
 368 | 
 369 | /* Enhanced secondary metrics */
 370 | .secondary-metrics {
 371 |   display: flex;
 372 |   gap: 1.5rem;
 373 |   align-items: baseline;
 374 | }
 375 | 
 376 | .secondary-metrics .metric {
 377 |   display: flex;
 378 |   flex-direction: column;
 379 |   align-items: center;
 380 |   text-align: center;
 381 |   min-width: 60px;
 382 | }
 383 | 
 384 | .secondary-metrics .metric .metric-count {
 385 |   font-weight: 600;
 386 |   color: #374151;
 387 |   line-height: 1.2;
 388 |   margin-bottom: 0.125rem;
 389 | }
 390 | 
 391 | .secondary-metrics .metric span:last-child {
 392 |   font-size: 0.6875rem;
 393 |   color: #64748b;
 394 |   font-weight: 500;
 395 |   text-transform: uppercase;
 396 |   letter-spacing: 0.05em;
 397 |   line-height: 1;
 398 | }
 399 | 
 400 | /* Definitions section */
 401 | .definitions {
 402 |   margin-bottom: 3rem;
 403 | }
 404 | 
 405 | .explanation-section {
 406 |   border: 1px solid var(--color-border);
 407 |   border-radius: 8px;
 408 |   background: #f5f3f0;
 409 |   overflow: hidden;
 410 | }
 411 | 
 412 | .explanation-toggle {
 413 |   width: 100%;
 414 |   padding: 0.75rem 1rem;
 415 |   background: transparent;
 416 |   border: none;
 417 |   display: flex;
 418 |   align-items: center;
 419 |   justify-content: space-between;
 420 |   cursor: pointer;
 421 |   font-family: "IBM Plex Mono", "SF Mono", Monaco, monospace;
 422 |   font-size: 0.75rem;
 423 |   font-weight: 500;
 424 |   color: #64748b;
 425 |   text-transform: uppercase;
 426 |   letter-spacing: 0.05em;
 427 |   transition: all 0.2s ease;
 428 | }
 429 | 
 430 | .explanation-toggle:hover {
 431 |   background: #f0ede9;
 432 |   color: #475569;
 433 | }
 434 | 
 435 | .explanation-toggle .toggle-text {
 436 |   text-transform: none;
 437 |   letter-spacing: 0;
 438 |   font-size: 0.875rem;
 439 |   color: var(--color-primary);
 440 | }
 441 | 
 442 | .explanation-toggle .toggle-arrow {
 443 |   font-size: 0.625rem;
 444 |   transition: transform 0.2s ease;
 445 | }
 446 | 
 447 | .explanation-toggle.expanded .toggle-arrow {
 448 |   transform: rotate(180deg);
 449 | }
 450 | 
 451 | .explanation-content {
 452 |   padding: 0;
 453 |   max-height: 0;
 454 |   overflow: hidden;
 455 |   transition: all 0.2s ease;
 456 |   background: var(--color-surface);
 457 | }
 458 | 
 459 | .explanation-content.expanded {
 460 |   padding: 1rem 1rem 1.25rem;
 461 |   max-height: 300px;
 462 | }
 463 | 
 464 | .explanation-content p {
 465 |   margin: 0 0 0.75rem 0;
 466 |   font-size: 0.875rem;
 467 |   line-height: 1.6;
 468 |   color: #374151;
 469 | }
 470 | 
 471 | .explanation-content p:last-child {
 472 |   margin-bottom: 0;
 473 | }
 474 | 
 475 | .explanation-content ul {
 476 |   margin: 0 0 0.75rem 0;
 477 |   padding-left: 1.25rem;
 478 | }
 479 | 
 480 | .explanation-content li {
 481 |   margin-bottom: 0.375rem;
 482 |   font-size: 0.875rem;
 483 |   line-height: 1.5;
 484 |   color: #4b5563;
 485 | }
 486 | 
 487 | .explanation-content strong {
 488 |   font-weight: 600;
 489 |   color: #1f2937;
 490 | }
 491 | 
 492 | .agent {
 493 |   background: var(--color-surface);
 494 |   border: none;
 495 |   border-bottom: 1px solid #f0ede9;
 496 |   border-radius: 0;
 497 |   padding: 1.125rem 1.5rem;
 498 |   margin-bottom: 0;
 499 |   transition: all 0.2s ease;
 500 |   position: relative;
 501 | }
 502 | 
 503 | .agent:last-child {
 504 |   border-bottom: none;
 505 | }
 506 | 
 507 | .agent:hover {
 508 |   background: #f6f4f1;
 509 |   transform: translateX(4px);
 510 |   padding-left: 1.75rem;
 511 | }
 512 | 
 513 | .agent-header {
 514 |   display: flex;
 515 |   align-items: center;
 516 |   gap: 1rem;
 517 |   margin-bottom: 0.625rem;
 518 | }
 519 | 
 520 | .rank {
 521 |   font-family: "IBM Plex Mono", "SF Mono", Monaco, monospace;
 522 |   font-size: 1rem;
 523 |   font-weight: 500;
 524 |   color: #4a5568;
 525 |   min-width: 40px;
 526 |   letter-spacing: 0.02em;
 527 | }
 528 | 
 529 | .agent-dot {
 530 |   width: 8px;
 531 |   height: 8px;
 532 |   border-radius: 50%;
 533 | }
 534 | 
 535 | .agent-info {
 536 |   flex: 1;
 537 | }
 538 | 
 539 | .agent-info h3 {
 540 |   font-family: "IBM Plex Mono", "SF Mono", Monaco, monospace;
 541 |   font-size: 0.9375rem;
 542 |   font-weight: 500;
 543 |   margin-bottom: 0.125rem;
 544 |   letter-spacing: 0.01em;
 545 | }
 546 | 
 547 | .agent-info h3 a {
 548 |   color: #1a202c;
 549 |   text-decoration: none;
 550 |   transition: color 0.2s ease;
 551 | }
 552 | 
 553 | .agent-info h3 a:hover {
 554 |   color: var(--color-accent);
 555 | }
 556 | 
 557 | .agent-subtitle {
 558 |   font-size: 0.75rem;
 559 |   color: #5a6674;
 560 |   font-weight: 400;
 561 |   display: none; /* Hide the redundant PR count */
 562 | }
 563 | 
 564 | .metrics {
 565 |   display: flex;
 566 |   justify-content: space-between;
 567 |   align-items: center;
 568 |   margin-bottom: 0.625rem;
 569 |   gap: 1.25rem;
 570 | }
 571 | 
 572 | .primary-metric {
 573 |   text-align: center;
 574 |   flex-shrink: 0;
 575 |   display: flex;
 576 |   flex-direction: column;
 577 |   align-items: center;
 578 |   justify-content: center;
 579 |   min-width: 80px;
 580 | }
 581 | 
 582 | .metric-value {
 583 |   font-family: "IBM Plex Mono", "SF Mono", Monaco, monospace;
 584 |   font-size: 1.75rem;
 585 |   font-weight: 500;
 586 |   color: #1a202c;
 587 |   display: block;
 588 |   line-height: 1;
 589 |   text-align: center;
 590 |   width: 100%;
 591 |   letter-spacing: 0.01em;
 592 | }
 593 | 
 594 | .metric-label {
 595 |   font-family: "IBM Plex Mono", "SF Mono", Monaco, monospace;
 596 |   font-size: 0.6875rem;
 597 |   color: #5a6674;
 598 |   font-weight: 500;
 599 |   text-transform: uppercase;
 600 |   letter-spacing: 0.1em;
 601 |   text-align: center;
 602 |   width: 100%;
 603 |   margin-top: 0.25rem;
 604 | }
 605 | 
 606 | .secondary-metrics {
 607 |   display: flex;
 608 |   gap: 1.5rem;
 609 |   align-items: center;
 610 |   flex: 1;
 611 |   justify-content: flex-end;
 612 | }
 613 | 
 614 | .metric {
 615 |   text-align: center;
 616 |   min-width: 60px;
 617 | }
 618 | 
 619 | .metric a {
 620 |   font-family: "IBM Plex Mono", "SF Mono", Monaco, monospace;
 621 |   color: #1a202c;
 622 |   text-decoration: none;
 623 |   font-weight: 500;
 624 |   font-size: 0.875rem;
 625 |   transition: all 0.15s ease;
 626 |   display: flex;
 627 |   flex-direction: column;
 628 |   align-items: center;
 629 |   text-align: center;
 630 |   line-height: 1;
 631 |   letter-spacing: 0.01em;
 632 | }
 633 | 
 634 | .metric a:hover {
 635 |   color: var(--color-accent);
 636 | }
 637 | 
 638 | .metric a:hover .metric-count {
 639 |   color: var(--color-accent);
 640 | }
 641 | 
 642 | .metric a:hover span:last-child {
 643 |   color: var(--color-accent);
 644 | }
 645 | 
 646 | /* Make "all PRs" metric more faint */
 647 | .metric.total-metric a {
 648 |   opacity: 0.7;
 649 | }
 650 | 
 651 | .metric.total-metric a:hover {
 652 |   opacity: 1;
 653 |   color: var(--color-accent);
 654 | }
 655 | 
 656 | .metric.total-metric a:hover .metric-count {
 657 |   color: var(--color-accent);
 658 | }
 659 | 
 660 | .metric.total-metric a:hover span:last-child {
 661 |   color: var(--color-accent);
 662 | }
 663 | 
 664 | /* Make "draft PRs" metric more faint since they're work in progress */
 665 | .metric.draft-metric a {
 666 |   opacity: 0.7;
 667 | }
 668 | 
 669 | .metric.draft-metric a:hover {
 670 |   opacity: 1;
 671 |   color: var(--color-accent);
 672 | }
 673 | 
 674 | .metric.draft-metric a:hover .metric-count {
 675 |   color: var(--color-accent);
 676 | }
 677 | 
 678 | .metric.draft-metric a:hover span:last-child {
 679 |   color: var(--color-accent);
 680 | }
 681 | 
 682 | .metric a .metric-count {
 683 |   font-family: "IBM Plex Mono", "SF Mono", Monaco, monospace;
 684 |   font-weight: 500;
 685 |   color: inherit;
 686 |   line-height: 1.2;
 687 |   margin-bottom: 0.125rem;
 688 |   font-size: 0.875rem;
 689 |   letter-spacing: 0.01em;
 690 |   transition: color 0.15s ease;
 691 | }
 692 | 
 693 | .metric a span:last-child {
 694 |   font-family: "IBM Plex Mono", "SF Mono", Monaco, monospace;
 695 |   display: block;
 696 |   font-size: 0.6875rem;
 697 |   color: #5a6674;
 698 |   margin-top: 0.25rem;
 699 |   font-weight: 500;
 700 |   text-transform: uppercase;
 701 |   letter-spacing: 0.08em;
 702 |   transition: color 0.15s ease;
 703 | }
 704 | 
 705 | .progress-bar {
 706 |   width: 100%;
 707 |   height: 4px;
 708 |   background: #edf2f7;
 709 |   border-radius: 2px;
 710 |   overflow: hidden;
 711 | }
 712 | 
 713 | .progress {
 714 |   height: 100%;
 715 |   border-radius: 2px;
 716 |   transition: width 0.3s ease;
 717 | }
 718 | 
 719 | /* Chart Section */
 720 | .chart-section {
 721 |   margin-bottom: 4rem;
 722 | }
 723 | 
 724 | .chart-section h2 {
 725 |   font-family: "Playfair Display", Georgia, serif;
 726 |   font-size: 1.875rem;
 727 |   font-weight: 600;
 728 |   color: #1a202c;
 729 |   margin-bottom: 1.5rem;
 730 |   text-align: left;
 731 |   letter-spacing: -0.02em;
 732 |   line-height: 1.2;
 733 | }
 734 | 
 735 | .chart {
 736 |   margin-bottom: 4rem;
 737 | }
 738 | 
 739 | .chart h2 {
 740 |   font-family: "Playfair Display", Georgia, serif;
 741 |   font-size: 2rem;
 742 |   font-weight: 600;
 743 |   color: #1a202c;
 744 |   margin-bottom: 1.5rem;
 745 |   text-align: left;
 746 |   letter-spacing: -0.02em;
 747 |   line-height: 1.2;
 748 | }
 749 | 
 750 | .chart-controls {
 751 |   margin: 15px 0;
 752 |   padding: 12px;
 753 |   background: #f5f3f0;
 754 |   border-radius: 6px;
 755 |   border: 1px solid var(--color-border);
 756 | }
 757 | 
 758 | .control-row {
 759 |   display: flex;
 760 |   justify-content: space-between;
 761 |   align-items: flex-start;
 762 |   gap: 15px;
 763 |   flex-wrap: wrap;
 764 | }
 765 | 
 766 | .control-section {
 767 |   display: flex;
 768 |   align-items: flex-start;
 769 |   gap: 8px;
 770 |   flex-direction: column;
 771 | }
 772 | 
 773 | .control-label {
 774 |   font-family: var(--font-mono);
 775 |   font-size: 9px;
 776 |   font-weight: 500;
 777 |   color: var(--color-secondary);
 778 |   text-transform: uppercase;
 779 |   letter-spacing: 0.08em;
 780 |   white-space: nowrap;
 781 |   margin-bottom: 2px;
 782 | }
 783 | 
 784 | .toggle-buttons {
 785 |   display: flex;
 786 |   gap: 4px;
 787 |   flex-wrap: wrap;
 788 |   align-items: center;
 789 | }
 790 | 
 791 | .toggle-btn,
 792 | .view-btn {
 793 |   font-family: var(--font-mono);
 794 |   display: inline-flex;
 795 |   align-items: center;
 796 |   gap: 0.375rem;
 797 |   padding: 0.25rem 0.5rem;
 798 |   border: 1px solid var(--color-border);
 799 |   border-radius: 4px;
 800 |   background: var(--color-surface);
 801 |   color: var(--color-secondary);
 802 |   font-size: 0.625rem;
 803 |   font-weight: 500;
 804 |   text-decoration: none;
 805 |   cursor: pointer;
 806 |   transition: all 0.2s ease;
 807 |   white-space: nowrap;
 808 |   user-select: none;
 809 |   letter-spacing: 0.025em;
 810 |   line-height: 1.2;
 811 | }
 812 | 
 813 | .toggle-btn:hover,
 814 | .view-btn:hover {
 815 |   border-color: var(--color-border-hover);
 816 |   color: #475569;
 817 |   background: #f6f4f1;
 818 |   box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
 819 | }
 820 | 
 821 | .toggle-btn.active {
 822 |   background: var(--color-accent);
 823 |   border-color: var(--color-accent);
 824 |   color: white;
 825 |   box-shadow: 0 1px 3px 0 rgba(124, 95, 163, 0.25);
 826 | }
 827 | 
 828 | .toggle-btn.active:hover {
 829 |   background: var(--color-accent-hover);
 830 |   border-color: var(--color-accent-hover);
 831 | }
 832 | 
 833 | .view-btn.active {
 834 |   background: #5a9a5a;
 835 |   border-color: #5a9a5a;
 836 |   color: white;
 837 |   box-shadow: 0 1px 3px 0 rgba(90, 154, 90, 0.25);
 838 | }
 839 | 
 840 | .view-btn.active:hover {
 841 |   background: #4a8a4a;
 842 |   border-color: #4a8a4a;
 843 | }
 844 | 
 845 | .toggle-icon,
 846 | .btn-icon {
 847 |   width: 5px;
 848 |   height: 5px;
 849 |   border-radius: 50%;
 850 |   display: inline-block;
 851 |   flex-shrink: 0;
 852 | }
 853 | 
 854 | .view-icon {
 855 |   font-size: 8px;
 856 |   line-height: 1;
 857 | }
 858 | 
 859 | .rate-btn {
 860 |   font-family: var(--font-mono);
 861 |   display: inline-flex;
 862 |   align-items: center;
 863 |   gap: 0.375rem;
 864 |   padding: 0.25rem 0.5rem;
 865 |   border: 1px solid var(--color-border);
 866 |   border-radius: 4px;
 867 |   background: var(--color-surface);
 868 |   color: var(--color-secondary);
 869 |   font-size: 0.625rem;
 870 |   font-weight: 500;
 871 |   text-decoration: none;
 872 |   cursor: pointer;
 873 |   transition: all 0.2s ease;
 874 |   white-space: nowrap;
 875 |   user-select: none;
 876 |   letter-spacing: 0.025em;
 877 |   line-height: 1.2;
 878 | }
 879 | 
 880 | .rate-btn:hover {
 881 |   border-color: var(--color-border-hover);
 882 |   color: #475569;
 883 |   background: #f6f4f1;
 884 |   box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
 885 | }
 886 | 
 887 | .rate-btn.active {
 888 |   background: #be5a5a;
 889 |   border-color: #be5a5a;
 890 |   color: white;
 891 |   box-shadow: 0 1px 3px 0 rgba(190, 90, 90, 0.25);
 892 | }
 893 | 
 894 | .rate-btn.active:hover {
 895 |   background: #a84a4a;
 896 |   border-color: #a84a4a;
 897 | }
 898 | 
 899 | .rate-icon {
 900 |   font-size: 8px;
 901 |   line-height: 1;
 902 | }
 903 | 
 904 | .chart-container {
 905 |   background: var(--color-surface);
 906 |   border: 1px solid var(--color-border);
 907 |   border-radius: 8px;
 908 |   padding: 1.5rem;
 909 |   height: 500px;
 910 |   position: relative;
 911 |   box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
 912 |   /* Force layout stability */
 913 |   min-width: 0;
 914 |   overflow: hidden;
 915 | }
 916 | 
 917 | .chart-container canvas {
 918 |   max-width: 100% !important;
 919 |   max-height: 100% !important;
 920 | }
 921 | 
 922 | /* Charts Section */
 923 | .charts-section {
 924 |   margin-bottom: 3rem;
 925 | }
 926 | 
 927 | .charts-container {
 928 |   display: grid;
 929 |   grid-template-columns: 1fr 1fr;
 930 |   gap: 1.25rem;
 931 |   margin-top: 1.25rem;
 932 | }
 933 | 
 934 | .chart-half {
 935 |   display: flex;
 936 |   flex-direction: column;
 937 | }
 938 | 
 939 | .chart-half h3 {
 940 |   font-family: "IBM Plex Mono", "SF Mono", Monaco, monospace;
 941 |   margin: 0 0 0.75rem 0;
 942 |   font-size: 1rem;
 943 |   font-weight: 500;
 944 |   color: #1a202c;
 945 |   text-align: center;
 946 |   letter-spacing: 0.01em;
 947 | }
 948 | 
 949 | /* Footer */
 950 | .footer {
 951 |   text-align: center;
 952 |   padding: 3rem 0;
 953 |   border-top: 1px solid var(--color-border);
 954 |   color: var(--color-secondary);
 955 |   background: transparent;
 956 |   margin-top: 4rem;
 957 |   font-size: 0.875rem;
 958 | }
 959 | 
 960 | .footer a {
 961 |   color: var(--color-accent);
 962 |   text-decoration: none;
 963 |   transition: color 0.2s ease;
 964 |   font-weight: 500;
 965 | }
 966 | 
 967 | .footer a:hover {
 968 |   color: var(--color-accent-hover);
 969 | }
 970 | 
 971 | /* Animations */
 972 | @keyframes fadeInUp {
 973 |   from {
 974 |     opacity: 0;
 975 |     transform: translateY(10px);
 976 |   }
 977 |   to {
 978 |     opacity: 1;
 979 |     transform: translateY(0);
 980 |   }
 981 | }
 982 | 
 983 | /* Large screen optimizations */
 984 | @media (min-width: 1400px) {
 985 |   .charts-container {
 986 |     gap: 2rem;
 987 |   }
 988 | 
 989 |   .chart-container {
 990 |     height: 600px;
 991 |     padding: 2rem;
 992 |   }
 993 | }
 994 | 
 995 | .agent {
 996 |   animation: fadeInUp 0.4s ease forwards;
 997 | }
 998 | 
 999 | .agent:nth-child(1) {
1000 |   animation-delay: 0.05s;
1001 | }
1002 | .agent:nth-child(2) {
1003 |   animation-delay: 0.1s;
1004 | }
1005 | .agent:nth-child(3) {
1006 |   animation-delay: 0.15s;
1007 | }
1008 | .agent:nth-child(4) {
1009 |   animation-delay: 0.2s;
1010 | }
1011 | .agent:nth-child(5) {
1012 |   animation-delay: 0.25s;
1013 | }
1014 | 
1015 | /* Responsive Design */
1016 | @media (max-width: 1024px) {
1017 |   .container {
1018 |     max-width: 800px;
1019 |     padding: 1.5rem 1.5rem;
1020 |   }
1021 | 
1022 |   .charts-container {
1023 |     gap: 1.25rem;
1024 |   }
1025 | 
1026 |   .chart-container {
1027 |     height: 450px;
1028 |   }
1029 | 
1030 |   .control-row {
1031 |     flex-direction: column;
1032 |     align-items: stretch;
1033 |     gap: 15px;
1034 |   }
1035 | 
1036 |   .control-section {
1037 |     flex-direction: column;
1038 |     align-items: stretch;
1039 |     gap: 6px;
1040 |   }
1041 | 
1042 |   .toggle-buttons {
1043 |     justify-content: flex-start;
1044 |     gap: 3px;
1045 |   }
1046 | }
1047 | 
1048 | @media (max-width: 768px) {
1049 |   .container {
1050 |     padding: 1.5rem 1rem;
1051 |   }
1052 | 
1053 |   .hero {
1054 |     padding-bottom: 2rem;
1055 |     margin-bottom: 3rem;
1056 |   }
1057 | 
1058 |   .hero h1 {
1059 |     font-size: 2rem;
1060 |   }
1061 | 
1062 |   .hero p {
1063 |     font-size: 1rem;
1064 |   }
1065 | 
1066 |   .leaderboard-header {
1067 |     flex-direction: column;
1068 |     align-items: flex-start;
1069 |     gap: 1rem;
1070 |   }
1071 | 
1072 |   .sort-controls {
1073 |     align-self: stretch;
1074 |   }
1075 | 
1076 |   .sort-btn {
1077 |     flex: 1;
1078 |     text-align: center;
1079 |   }
1080 | 
1081 |   .agent {
1082 |     padding: 1rem 1.25rem;
1083 |   }
1084 | 
1085 |   .agent:hover {
1086 |     padding-left: 1.5rem;
1087 |   }
1088 | 
1089 |   .metrics {
1090 |     flex-direction: column;
1091 |     gap: 1rem;
1092 |     text-align: center;
1093 |   }
1094 | 
1095 |   .secondary-metrics {
1096 |     justify-content: center;
1097 |     gap: 1.25rem;
1098 |     width: 100%;
1099 |   }
1100 | 
1101 |   .metric {
1102 |     min-width: 55px;
1103 |   }
1104 | 
1105 |   .rank {
1106 |     font-size: 1.2rem;
1107 |     min-width: 40px;
1108 |   }
1109 | 
1110 |   .charts-container {
1111 |     grid-template-columns: 1fr;
1112 |     gap: 1.25rem;
1113 |   }
1114 | 
1115 |   .chart-container {
1116 |     height: 350px;
1117 |     padding: 1rem;
1118 |   }
1119 | 
1120 |   .toggle-btn,
1121 |   .view-btn,
1122 |   .rate-btn {
1123 |     padding: 0.25rem 0.4rem;
1124 |     font-size: 0.6rem;
1125 |     gap: 0.3rem;
1126 |   }
1127 | 
1128 |   .toggle-buttons {
1129 |     gap: 3px;
1130 |   }
1131 | 
1132 |   .sort-btn {
1133 |     padding: 0.4375rem 0.75rem;
1134 |     font-size: 0.6875rem;
1135 |   }
1136 | 
1137 |   .chart-controls {
1138 |     padding: 12px;
1139 |     margin: 15px 0;
1140 |   }
1141 | 
1142 |   .agent-row {
1143 |     flex-direction: column;
1144 |     text-align: center;
1145 |     padding: 1rem;
1146 |   }
1147 | 
1148 |   .agent-info {
1149 |     margin: 0.5rem 0;
1150 |   }
1151 | 
1152 |   .agent-stats {
1153 |     justify-content: center;
1154 |     margin-top: 1rem;
1155 |   }
1156 | }
1157 | 
1158 | @media (max-width: 480px) {
1159 |   .agent-header {
1160 |     flex-wrap: wrap;
1161 |   }
1162 | 
1163 |   .secondary-metrics {
1164 |     flex-direction: column;
1165 |     gap: 0.5rem;
1166 |   }
1167 | 
1168 |   .chart-container {
1169 |     height: 280px;
1170 |     padding: 0.75rem;
1171 |   }
1172 | 
1173 |   .toggle-btn,
1174 |   .view-btn,
1175 |   .rate-btn {
1176 |     padding: 0.2rem 0.35rem;
1177 |     font-size: 0.55rem;
1178 |     gap: 0.25rem;
1179 |   }
1180 | 
1181 |   .toggle-buttons {
1182 |     gap: 2px;
1183 |   }
1184 | 
1185 |   .sort-btn {
1186 |     padding: 0.375rem 0.625rem;
1187 |     font-size: 0.625rem;
1188 |   }
1189 | 
1190 |   .control-label {
1191 |     font-size: 10px;
1192 |   }
1193 | }
1194 | 


--------------------------------------------------------------------------------
/generate_chart.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | # PR‑tracker: generates a combo chart from the collected PR data.
  3 | # deps: pandas, matplotlib, numpy
  4 | 
  5 | from pathlib import Path
  6 | import pandas as pd
  7 | import matplotlib
  8 | 
  9 | matplotlib.use("Agg")  # headless
 10 | import matplotlib.pyplot as plt
 11 | import numpy as np
 12 | import datetime as dt
 13 | import re
 14 | import json
 15 | from jinja2 import Environment, FileSystemLoader
 16 | 
 17 | 
 18 | TEMPLATE_DIR = Path("templates")
 19 | env = Environment(loader=FileSystemLoader(TEMPLATE_DIR))
 20 | env.filters["comma"] = lambda v: f"{int(v):,}" if isinstance(v, (int, float)) else v
 21 | 
 22 | AGENTS = [
 23 |     {
 24 |         "key": "copilot",
 25 |         "display": "Copilot",
 26 |         "long_name": "GitHub Copilot",
 27 |         "color": "#2563eb",
 28 |         "info_url": "https://docs.github.com/en/copilot/using-github-copilot/coding-agent/using-copilot-to-work-on-an-issue",
 29 |         "total_query_url": "https://github.com/search?q=is:pr+head:copilot/&type=pullrequests",
 30 |         "merged_query_url": "https://github.com/search?q=is:pr+head:copilot/+is:merged&type=pullrequests",
 31 |         "ready_query_url": "https://github.com/search?q=is:pr+head:copilot/+-is:draft&type=pullrequests",
 32 |         "draft_query_url": "https://github.com/search?q=is:pr+head:copilot/+is:draft&type=pullrequests",
 33 |     },
 34 |     {
 35 |         "key": "codex",
 36 |         "display": "Codex",
 37 |         "long_name": "OpenAI Codex",
 38 |         "color": "#dc2626",
 39 |         "info_url": "https://openai.com/index/introducing-codex/",
 40 |         "total_query_url": "https://github.com/search?q=is:pr+head:codex/&type=pullrequests",
 41 |         "merged_query_url": "https://github.com/search?q=is:pr+head:codex/+is:merged&type=pullrequests",
 42 |         "ready_query_url": "https://github.com/search?q=is:pr+head:codex/+-is:draft&type=pullrequests",
 43 |         "draft_query_url": "https://github.com/search?q=is:pr+head:codex/+is:draft&type=pullrequests",
 44 |     },
 45 |     {
 46 |         "key": "cursor",
 47 |         "display": "Cursor",
 48 |         "long_name": "Cursor Agents",
 49 |         "color": "#7c3aed",
 50 |         "info_url": "https://docs.cursor.com/background-agent",
 51 |         "total_query_url": "https://github.com/search?q=is:pr+head:cursor/&type=pullrequests",
 52 |         "merged_query_url": "https://github.com/search?q=is:pr+head:cursor/+is:merged&type=pullrequests",
 53 |         "ready_query_url": "https://github.com/search?q=is:pr+head:cursor/+-is:draft&type=pullrequests",
 54 |         "draft_query_url": "https://github.com/search?q=is:pr+head:cursor/+is:draft&type=pullrequests",
 55 |     },
 56 |     {
 57 |         "key": "devin",
 58 |         "display": "Devin",
 59 |         "long_name": "Devin",
 60 |         "color": "#059669",
 61 |         "info_url": "https://devin.ai/pricing",
 62 |         "total_query_url": "https://github.com/search?q=is:pr+author:devin-ai-integration[bot]&type=pullrequests",
 63 |         "merged_query_url": "https://github.com/search?q=is:pr+author:devin-ai-integration[bot]+is:merged&type=pullrequests",
 64 |         "ready_query_url": "https://github.com/search?q=is:pr+author:devin-ai-integration[bot]+-is:draft&type=pullrequests",
 65 |         "draft_query_url": "https://github.com/search?q=is:pr+author:devin-ai-integration[bot]+is:draft&type=pullrequests",
 66 |     },
 67 |     {
 68 |         "key": "codegen",
 69 |         "display": "Codegen",
 70 |         "long_name": "Codegen",
 71 |         "color": "#d97706",
 72 |         "info_url": "https://codegen.com/",
 73 |         "total_query_url": "https://github.com/search?q=is:pr+author:codegen-sh[bot]&type=pullrequests",
 74 |         "merged_query_url": "https://github.com/search?q=is:pr+author:codegen-sh[bot]+is:merged&type=pullrequests",
 75 |         "ready_query_url": "https://github.com/search?q=is:pr+author:codegen-sh[bot]+-is:draft&type=pullrequests",
 76 |         "draft_query_url": "https://github.com/search?q=is:pr+author:codegen-sh[bot]+is:draft&type=pullrequests",
 77 |     },
 78 | ]
 79 | 
 80 | 
 81 | def build_stats(latest, df=None):
 82 |     stats = {}
 83 | 
 84 |     # Get real data for each agent
 85 |     for agent in AGENTS:
 86 |         key = agent["key"]
 87 |         total = int(latest[f"{key}_total"])
 88 |         merged = int(latest[f"{key}_merged"])
 89 |         nondraft = (
 90 |             int(latest[f"{key}_nondraft"]) if f"{key}_nondraft" in latest else total
 91 |         )
 92 | 
 93 |         # Calculate rates for different PR types
 94 |         total_rate = (merged / total * 100) if total > 0 else 0
 95 |         ready_rate = (merged / nondraft * 100) if nondraft > 0 else 0
 96 | 
 97 |         stats[key] = {
 98 |             "total": total,
 99 |             "merged": merged,
100 |             "nondraft": nondraft,  # ready PRs (non-draft)
101 |             "rate": ready_rate,  # Default to ready PR success rate
102 |             "total_rate": total_rate,  # Success rate including drafts
103 |             "ready_rate": ready_rate,  # Success rate for ready PRs only
104 |         }
105 |     return stats
106 | 
107 | 
108 | def generate_chart(csv_file=None):
109 |     # Default to data.csv if no file specified
110 |     if csv_file is None:
111 |         csv_file = Path("data.csv")
112 | 
113 |     # Ensure file exists
114 |     if not csv_file.exists():
115 |         print(f"Error: {csv_file} not found.")
116 |         print("Run collect_data.py first to collect data.")
117 |         return False
118 | 
119 |     # Create chart
120 |     df = pd.read_csv(csv_file)
121 |     # Fix timestamp format - replace special dash characters with regular hyphens
122 |     df["timestamp"] = df["timestamp"].str.replace("‑", "-")
123 |     df["timestamp"] = pd.to_datetime(df["timestamp"])
124 | 
125 |     # Check if data exists
126 |     if len(df) == 0:
127 |         print("Error: No data found in CSV file.")
128 |         return False
129 | 
130 |     # Limit to 8 data points spread across the entire dataset to avoid chart getting too busy
131 |     total_points = len(df)
132 |     if total_points > 8:
133 |         # Create evenly spaced indices across the entire dataset
134 |         indices = np.linspace(0, total_points - 1, num=8, dtype=int)
135 |         df = df.iloc[indices]
136 |         print(
137 |             f"Limited chart to 8 data points evenly distributed across {total_points} total points."
138 |         )
139 | 
140 |     # Calculate percentages with safety checks - both ready and total rates
141 |     # Ready rate (merged/nondraft) - default for chart display
142 |     df["copilot_percentage"] = df.apply(
143 |         lambda row: (
144 |             (row["copilot_merged"] / row["copilot_nondraft"] * 100)
145 |             if row["copilot_nondraft"] > 0
146 |             else 0
147 |         ),
148 |         axis=1,
149 |     )
150 |     df["codex_percentage"] = df.apply(
151 |         lambda row: (
152 |             (row["codex_merged"] / row["codex_nondraft"] * 100)
153 |             if row["codex_nondraft"] > 0
154 |             else 0
155 |         ),
156 |         axis=1,
157 |     )
158 |     df["cursor_percentage"] = df.apply(
159 |         lambda row: (
160 |             (row["cursor_merged"] / row["cursor_nondraft"] * 100)
161 |             if row["cursor_nondraft"] > 0
162 |             else 0
163 |         ),
164 |         axis=1,
165 |     )
166 |     df["devin_percentage"] = df.apply(
167 |         lambda row: (
168 |             (row["devin_merged"] / row["devin_nondraft"] * 100)
169 |             if row["devin_nondraft"] > 0
170 |             else 0
171 |         ),
172 |         axis=1,
173 |     )
174 |     df["codegen_percentage"] = df.apply(
175 |         lambda row: (
176 |             (row["codegen_merged"] / row["codegen_nondraft"] * 100)
177 |             if row["codegen_nondraft"] > 0
178 |             else 0
179 |         ),
180 |         axis=1,
181 |     )
182 | 
183 |     # Total rate (merged/total) - for alternative view
184 |     df["copilot_total_percentage"] = df.apply(
185 |         lambda row: (
186 |             (row["copilot_merged"] / row["copilot_total"] * 100)
187 |             if row["copilot_total"] > 0
188 |             else 0
189 |         ),
190 |         axis=1,
191 |     )
192 |     df["codex_total_percentage"] = df.apply(
193 |         lambda row: (
194 |             (row["codex_merged"] / row["codex_total"] * 100)
195 |             if row["codex_total"] > 0
196 |             else 0
197 |         ),
198 |         axis=1,
199 |     )
200 |     df["cursor_total_percentage"] = df.apply(
201 |         lambda row: (
202 |             (row["cursor_merged"] / row["cursor_total"] * 100)
203 |             if row["cursor_total"] > 0
204 |             else 0
205 |         ),
206 |         axis=1,
207 |     )
208 |     df["devin_total_percentage"] = df.apply(
209 |         lambda row: (
210 |             (row["devin_merged"] / row["devin_total"] * 100)
211 |             if row["devin_total"] > 0
212 |             else 0
213 |         ),
214 |         axis=1,
215 |     )
216 |     df["codegen_total_percentage"] = df.apply(
217 |         lambda row: (
218 |             (row["codegen_merged"] / row["codegen_total"] * 100)
219 |             if row["codegen_total"] > 0
220 |             else 0
221 |         ),
222 |         axis=1,
223 |     )
224 | 
225 |     # Adjust chart size based on data points, adding extra space for legends
226 |     num_points = len(df)
227 |     if num_points <= 3:
228 |         fig_width = max(12, num_points * 4)  # Increased from 10 to 12
229 |         fig_height = 8  # Increased from 6 to 8
230 |     else:
231 |         fig_width = 16  # Increased from 14 to 16
232 |         fig_height = 10  # Increased from 8 to 10
233 | 
234 |     # Create the combination chart
235 |     fig, ax1 = plt.subplots(figsize=(fig_width, fig_height))
236 |     ax2 = ax1.twinx()
237 | 
238 |     # Prepare data
239 |     x = np.arange(len(df))
240 |     # Adjust bar width based on number of data points (5 groups now)
241 |     width = min(0.16, 0.8 / max(1, num_points * 0.6))
242 | 
243 |     # Bar charts for totals and merged
244 |     bars_copilot_total = ax1.bar(
245 |         x - 2 * width,
246 |         df["copilot_total"],
247 |         width,
248 |         label="Copilot Total",
249 |         alpha=0.7,
250 |         color="#93c5fd",
251 |     )
252 |     bars_copilot_merged = ax1.bar(
253 |         x - 2 * width,
254 |         df["copilot_merged"],
255 |         width,
256 |         label="Copilot Merged",
257 |         alpha=1.0,
258 |         color="#2563eb",
259 |     )
260 | 
261 |     bars_codex_total = ax1.bar(
262 |         x - 1 * width,
263 |         df["codex_total"],
264 |         width,
265 |         label="Codex Total",
266 |         alpha=0.7,
267 |         color="#fca5a5",
268 |     )
269 |     bars_codex_merged = ax1.bar(
270 |         x - 1 * width,
271 |         df["codex_merged"],
272 |         width,
273 |         label="Codex Merged",
274 |         alpha=1.0,
275 |         color="#dc2626",
276 |     )
277 | 
278 |     bars_cursor_total = ax1.bar(
279 |         x + 0 * width,
280 |         df["cursor_total"],
281 |         width,
282 |         label="Cursor Total",
283 |         alpha=0.7,
284 |         color="#c4b5fd",
285 |     )
286 |     bars_cursor_merged = ax1.bar(
287 |         x + 0 * width,
288 |         df["cursor_merged"],
289 |         width,
290 |         label="Cursor Merged",
291 |         alpha=1.0,
292 |         color="#7c3aed",
293 |     )
294 | 
295 |     bars_devin_total = ax1.bar(
296 |         x + 1 * width,
297 |         df["devin_total"],
298 |         width,
299 |         label="Devin Total",
300 |         alpha=0.7,
301 |         color="#86efac",
302 |     )
303 |     bars_devin_merged = ax1.bar(
304 |         x + 1 * width,
305 |         df["devin_merged"],
306 |         width,
307 |         label="Devin Merged",
308 |         alpha=1.0,
309 |         color="#059669",
310 |     )
311 | 
312 |     bars_codegen_total = ax1.bar(
313 |         x + 2 * width,
314 |         df["codegen_total"],
315 |         width,
316 |         label="Codegen Total",
317 |         alpha=0.7,
318 |         color="#fed7aa",
319 |     )
320 |     bars_codegen_merged = ax1.bar(
321 |         x + 2 * width,
322 |         df["codegen_merged"],
323 |         width,
324 |         label="Codegen Merged",
325 |         alpha=1.0,
326 |         color="#d97706",
327 |     )
328 | 
329 |     # Line charts for percentages (on secondary y-axis)
330 |     line_copilot = ax2.plot(
331 |         x,
332 |         df["copilot_percentage"],
333 |         "o-",
334 |         color="#1d4ed8",
335 |         linewidth=3,
336 |         markersize=10,
337 |         label="Copilot Success %",
338 |         markerfacecolor="white",
339 |         markeredgewidth=2,
340 |         markeredgecolor="#1d4ed8",
341 |     )
342 | 
343 |     line_codex = ax2.plot(
344 |         x,
345 |         df["codex_percentage"],
346 |         "s-",
347 |         color="#b91c1c",
348 |         linewidth=3,
349 |         markersize=10,
350 |         label="Codex Success %",
351 |         markerfacecolor="white",
352 |         markeredgewidth=2,
353 |         markeredgecolor="#b91c1c",
354 |     )
355 | 
356 |     line_cursor = ax2.plot(
357 |         x,
358 |         df["cursor_percentage"],
359 |         "d-",
360 |         color="#6d28d9",
361 |         linewidth=3,
362 |         markersize=10,
363 |         label="Cursor Success %",
364 |         markerfacecolor="white",
365 |         markeredgewidth=2,
366 |         markeredgecolor="#6d28d9",
367 |     )
368 | 
369 |     line_devin = ax2.plot(
370 |         x,
371 |         df["devin_percentage"],
372 |         "^-",
373 |         color="#047857",
374 |         linewidth=3,
375 |         markersize=10,
376 |         label="Devin Success %",
377 |         markerfacecolor="white",
378 |         markeredgewidth=2,
379 |         markeredgecolor="#047857",
380 |     )
381 | 
382 |     line_codegen = ax2.plot(
383 |         x,
384 |         df["codegen_percentage"],
385 |         "v-",
386 |         color="#b45309",
387 |         linewidth=3,
388 |         markersize=10,
389 |         label="Codegen Success %",
390 |         markerfacecolor="white",
391 |         markeredgewidth=2,
392 |         markeredgecolor="#b45309",
393 |     )
394 | 
395 |     # Customize the chart
396 |     ax1.set_xlabel("Data Points", fontsize=12, fontweight="bold")
397 |     ax1.set_ylabel(
398 |         "PR Counts (Total & Merged)", fontsize=12, fontweight="bold", color="black"
399 |     )
400 |     ax2.set_ylabel(
401 |         "Merge Success Rate (%)", fontsize=12, fontweight="bold", color="black"
402 |     )
403 | 
404 |     title = "PR Analytics: Volume vs Success Rate Comparison"
405 |     ax1.set_title(title, fontsize=16, fontweight="bold", pad=20)
406 | 
407 |     # Set x-axis labels with timestamps
408 |     timestamps = df["timestamp"].dt.strftime("%m-%d %H:%M")
409 |     ax1.set_xticks(x)
410 |     ax1.set_xticklabels(timestamps, rotation=45)
411 | 
412 |     # Add legends - move name labels to top left, success % labels to bottom right
413 |     # Position legends further outside with more padding
414 |     legend1 = ax1.legend(loc="upper left", bbox_to_anchor=(-0.15, 1.15))
415 |     legend2 = ax2.legend(loc="lower right", bbox_to_anchor=(1.15, -0.15))
416 | 
417 |     # Add grid
418 |     ax1.grid(True, alpha=0.3, linestyle="--")
419 | 
420 |     # Set percentage axis range
421 |     ax2.set_ylim(0, 100)
422 | 
423 |     # Add value labels on bars (with safety checks)
424 |     def add_value_labels(ax, bars, format_str="{:.0f}"):
425 |         for bar in bars:
426 |             height = bar.get_height()
427 |             if height > 0:
428 |                 # Ensure the label fits within reasonable bounds
429 |                 label_text = format_str.format(height)
430 |                 if len(label_text) > 10:  # Truncate very long numbers
431 |                     if height >= 1000:
432 |                         label_text = f"{height/1000:.1f}k"
433 |                     elif height >= 1000000:
434 |                         label_text = f"{height/1000000:.1f}M"
435 | 
436 |                 ax.text(
437 |                     bar.get_x() + bar.get_width() / 2.0,
438 |                     height,
439 |                     label_text,
440 |                     ha="center",
441 |                     va="bottom",
442 |                     fontsize=8,
443 |                     fontweight="normal",
444 |                     color="black",
445 |                 )
446 | 
447 |     add_value_labels(ax1, bars_copilot_total)
448 |     add_value_labels(ax1, bars_copilot_merged)
449 |     add_value_labels(ax1, bars_codex_total)
450 |     add_value_labels(ax1, bars_codex_merged)
451 |     add_value_labels(ax1, bars_cursor_total)
452 |     add_value_labels(ax1, bars_cursor_merged)
453 |     add_value_labels(ax1, bars_devin_total)
454 |     add_value_labels(ax1, bars_devin_merged)
455 |     add_value_labels(ax1, bars_codegen_total)
456 |     add_value_labels(ax1, bars_codegen_merged)
457 | 
458 |     # Add percentage labels on line points (with validation and skip 0.0%)
459 |     for i, (cop_pct, cod_pct, cur_pct, dev_pct, cg_pct) in enumerate(
460 |         zip(
461 |             df["copilot_percentage"],
462 |             df["codex_percentage"],
463 |             df["cursor_percentage"],
464 |             df["devin_percentage"],
465 |             df["codegen_percentage"],
466 |         )
467 |     ):
468 |         # Only add labels if percentages are valid numbers and not 0.0%
469 |         if (
470 |             pd.notna(cop_pct)
471 |             and pd.notna(cod_pct)
472 |             and pd.notna(cur_pct)
473 |             and pd.notna(dev_pct)
474 |             and pd.notna(cg_pct)
475 |         ):
476 |             if cop_pct > 0.0:
477 |                 ax2.annotate(
478 |                     f"{cop_pct:.1f}%",
479 |                     (i, cop_pct),
480 |                     textcoords="offset points",
481 |                     xytext=(0, 15),
482 |                     ha="center",
483 |                     fontsize=10,
484 |                     fontweight="bold",
485 |                     color="#1d4ed8",
486 |                 )
487 |             if cod_pct > 0.0:
488 |                 ax2.annotate(
489 |                     f"{cod_pct:.1f}%",
490 |                     (i, cod_pct),
491 |                     textcoords="offset points",
492 |                     xytext=(0, -20),
493 |                     ha="center",
494 |                     fontsize=10,
495 |                     fontweight="bold",
496 |                     color="#b91c1c",
497 |                 )
498 |             if cur_pct > 0.0:
499 |                 ax2.annotate(
500 |                     f"{cur_pct:.1f}%",
501 |                     (i, cur_pct),
502 |                     textcoords="offset points",
503 |                     xytext=(0, -35),
504 |                     ha="center",
505 |                     fontsize=10,
506 |                     fontweight="bold",
507 |                     color="#6d28d9",
508 |                 )
509 |             if dev_pct > 0.0:
510 |                 ax2.annotate(
511 |                     f"{dev_pct:.1f}%",
512 |                     (i, dev_pct),
513 |                     textcoords="offset points",
514 |                     xytext=(0, -50),
515 |                     ha="center",
516 |                     fontsize=10,
517 |                     fontweight="bold",
518 |                     color="#047857",
519 |                 )
520 |             if cg_pct > 0.0:
521 |                 ax2.annotate(
522 |                     f"{cg_pct:.1f}%",
523 |                     (i, cg_pct),
524 |                     textcoords="offset points",
525 |                     xytext=(0, -65),
526 |                     ha="center",
527 |                     fontsize=10,
528 |                     fontweight="bold",
529 |                     color="#b45309",
530 |                 )
531 | 
532 |     plt.tight_layout(pad=6.0)
533 | 
534 |     # Adjust subplot parameters to ensure legends fit entirely outside the chart
535 |     plt.subplots_adjust(left=0.2, right=0.85, top=0.85, bottom=0.2)
536 | 
537 |     # Save chart to docs directory (single location for both README and GitHub Pages)
538 |     docs_dir = Path("docs")
539 |     docs_dir.mkdir(exist_ok=True)  # Ensure docs directory exists
540 |     chart_file = docs_dir / "chart.png"
541 |     dpi = 150 if num_points <= 5 else 300
542 |     fig.savefig(chart_file, dpi=dpi, bbox_inches="tight", facecolor="white")
543 |     print(f"Chart generated: {chart_file}")
544 | 
545 |     # Export chart data as JSON for interactive chart
546 |     export_chart_data_json(df)
547 | 
548 |     # Update the README with latest statistics
549 |     update_readme(df)
550 | 
551 |     # Update the GitHub Pages with latest statistics
552 |     update_github_pages(df)
553 | 
554 |     return True
555 | 
556 | 
557 | def export_chart_data_json(df):
558 |     """Export chart data as JSON for interactive JavaScript chart"""
559 |     docs_dir = Path("docs")
560 |     docs_dir.mkdir(exist_ok=True)
561 | 
562 |     # Prepare data for Chart.js
563 |     chart_data = {"labels": [], "datasets": []}
564 | 
565 |     # Format timestamps for labels
566 |     for _, row in df.iterrows():
567 |         timestamp = row["timestamp"]
568 |         if isinstance(timestamp, str):
569 |             timestamp = pd.to_datetime(timestamp)
570 |         chart_data["labels"].append(timestamp.strftime("%m/%d %H:%M"))
571 | 
572 |     # Color scheme matching the Python chart - elegant professional colors
573 |     colors = {
574 |         "copilot": {"total": "#93c5fd", "merged": "#2563eb", "line": "#1d4ed8"},
575 |         "codex": {"total": "#fca5a5", "merged": "#dc2626", "line": "#b91c1c"},
576 |         "cursor": {"total": "#c4b5fd", "merged": "#7c3aed", "line": "#6d28d9"},
577 |         "devin": {"total": "#86efac", "merged": "#059669", "line": "#047857"},
578 |         "codegen": {"total": "#fed7aa", "merged": "#d97706", "line": "#b45309"},
579 |     }
580 | 
581 |     # Add bar datasets for totals and merged PRs
582 |     for agent in ["copilot", "codex", "cursor", "devin", "codegen"]:
583 |         # Process data to replace leading zeros with None (null in JSON)
584 |         total_data = df[f"{agent}_total"].tolist()
585 |         merged_data = df[f"{agent}_merged"].tolist()
586 |         ready_percentage_data = df[f"{agent}_percentage"].tolist()  # ready rate
587 |         total_percentage_data = df[f"{agent}_total_percentage"].tolist()  # total rate
588 | 
589 |         # Find first non-zero total value index
590 |         first_nonzero_idx = None
591 |         for i, total in enumerate(total_data):
592 |             if total > 0:
593 |                 first_nonzero_idx = i
594 |                 break
595 | 
596 |         # Replace leading zeros with None
597 |         if first_nonzero_idx is not None:
598 |             for i in range(first_nonzero_idx):
599 |                 total_data[i] = None
600 |                 merged_data[i] = None
601 |                 ready_percentage_data[i] = None
602 |                 total_percentage_data[i] = None
603 | 
604 |         # Total PRs
605 |         chart_data["datasets"].append(
606 |             {
607 |                 "label": f"{agent.title()} Total",
608 |                 "type": "bar",
609 |                 "data": total_data,
610 |                 "backgroundColor": colors[agent]["total"],
611 |                 "borderColor": colors[agent]["total"],
612 |                 "borderWidth": 1,
613 |                 "yAxisID": "y",
614 |                 "order": 2,
615 |             }
616 |         )
617 | 
618 |         # Merged PRs
619 |         chart_data["datasets"].append(
620 |             {
621 |                 "label": f"{agent.title()} Merged",
622 |                 "type": "bar",
623 |                 "data": merged_data,
624 |                 "backgroundColor": colors[agent]["merged"],
625 |                 "borderColor": colors[agent]["merged"],
626 |                 "borderWidth": 1,
627 |                 "yAxisID": "y",
628 |                 "order": 2,
629 |             }
630 |         )
631 | 
632 |         # Success rate line (ready PRs) - shown by default
633 |         chart_data["datasets"].append(
634 |             {
635 |                 "label": f"{agent.title()} Success % (Ready)",
636 |                 "type": "line",
637 |                 "data": ready_percentage_data,
638 |                 "borderColor": colors[agent]["line"],
639 |                 "backgroundColor": "rgba(255, 255, 255, 0.8)",
640 |                 "borderWidth": 3,
641 |                 "pointRadius": 3,
642 |                 "pointHoverRadius": 5,
643 |                 "fill": False,
644 |                 "yAxisID": "y1",
645 |                 "order": 1,
646 |                 "rateType": "ready",
647 |             }
648 |         )
649 | 
650 |         # Success rate line (all PRs) - hidden by default
651 |         chart_data["datasets"].append(
652 |             {
653 |                 "label": f"{agent.title()} Success % (All)",
654 |                 "type": "line",
655 |                 "data": total_percentage_data,
656 |                 "borderColor": colors[agent]["line"],
657 |                 "backgroundColor": "rgba(255, 255, 255, 0.8)",
658 |                 "borderWidth": 3,
659 |                 "pointRadius": 3,
660 |                 "pointHoverRadius": 5,
661 |                 "fill": False,
662 |                 "yAxisID": "y1",
663 |                 "order": 1,
664 |                 "hidden": True,  # Hidden by default
665 |                 "rateType": "total",
666 |             }
667 |         )
668 | 
669 |     # Write JSON file
670 |     json_file = docs_dir / "chart-data.json"
671 |     with open(json_file, "w") as f:
672 |         json.dump(chart_data, f, indent=2)
673 | 
674 |     print(f"Chart data exported: {json_file}")
675 |     return True
676 | 
677 | 
678 | def update_readme(df):
679 |     """Render README.md from template with latest statistics"""
680 |     readme_path = Path("README.md")
681 |     if not readme_path.exists():
682 |         print(f"Warning: {readme_path} not found, skipping README update.")
683 |         return False
684 | 
685 |     latest = df.iloc[-1]
686 |     stats = build_stats(latest)
687 | 
688 |     context = {"agents": AGENTS, "stats": stats}
689 |     content = env.get_template("readme_template.md").render(context)
690 |     readme_path.write_text(content)
691 |     print("README.md updated with latest statistics.")
692 |     return True
693 | 
694 | 
695 | def update_github_pages(df):
696 |     """Render the GitHub Pages site from template with latest statistics"""
697 |     index_path = Path("docs/index.html")
698 |     if not index_path.exists():
699 |         print(f"Warning: {index_path} not found, skipping GitHub Pages update.")
700 |         return False
701 | 
702 |     latest = df.iloc[-1]
703 |     stats = build_stats(latest)
704 |     timestamp = dt.datetime.now().strftime("%B %d, %Y %H:%M UTC")
705 | 
706 |     # Simple context - just the essentials
707 |     context = {"agents": AGENTS, "stats": stats, "timestamp": timestamp}
708 | 
709 |     content = env.get_template("index_template.html").render(context)
710 |     index_path.write_text(content)
711 |     print("GitHub Pages updated with latest statistics and enhanced analytics.")
712 |     return True
713 | 
714 | 
715 | if __name__ == "__main__":
716 |     generate_chart()
717 | 


--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | matplotlib
2 | pandas
3 | requests
4 | numpy
5 | jinja2
6 | 


--------------------------------------------------------------------------------
/scripts/add_nondraft_final.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | """
  3 | ONE-TIME SCRIPT: Add non-draft PR data to existing data.csv
  4 | Used to retroactively add nondraft columns to historical data by querying GitHub API.
  5 | This script strategically samples data points and handles GitHub API limitations properly.
  6 | Completed: Added non-draft tracking for all agents in the dataset.
  7 | """
  8 | 
  9 | import csv
 10 | import datetime as dt
 11 | import time
 12 | import os
 13 | from pathlib import Path
 14 | import requests
 15 | from typing import Dict
 16 | 
 17 | # GitHub API headers - include PAT if available
 18 | HEADERS = {"Accept": "application/vnd.github+json", "User-Agent": "PR-Watcher"}
 19 | if github_token := os.getenv("GITHUB_TOKEN"):
 20 |     HEADERS["Authorization"] = f"token {github_token}"
 21 |     print("✅ Using GitHub PAT for authentication")
 22 | else:
 23 |     print("⚠️  No GitHub PAT found, using unauthenticated requests")
 24 | 
 25 | # Non-draft queries (excluding drafts with -is:draft)
 26 | NONDRAFT_QUERIES = {
 27 |     "is:pr+head:copilot/+-is:draft": "copilot_nondraft",
 28 |     "is:pr+head:codex/+-is:draft": "codex_nondraft",
 29 |     "is:pr+head:cursor/+-is:draft": "cursor_nondraft",
 30 |     "is:pr+author:devin-ai-integration[bot]+-is:draft": "devin_nondraft",
 31 |     "is:pr+author:codegen-sh[bot]+-is:draft": "codegen_nondraft",
 32 | }
 33 | 
 34 | 
 35 | def parse_timestamp(timestamp_str: str) -> dt.datetime:
 36 |     """Parse CSV timestamp to datetime object."""
 37 |     return dt.datetime.strptime(timestamp_str.replace("‑", "-"), "%Y-%m-%d %H:%M:%S")
 38 | 
 39 | 
 40 | def format_github_date(timestamp: dt.datetime) -> str:
 41 |     """Format datetime for GitHub API (ISO format with UTC)."""
 42 |     return timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
 43 | 
 44 | 
 45 | def test_single_query():
 46 |     """Test a single query to verify our approach works."""
 47 |     print("Testing single query...")
 48 |     test_query = "is:pr+head:copilot/+-is:draft"
 49 | 
 50 |     try:
 51 |         r = requests.get(
 52 |             f"https://api.github.com/search/issues?q={test_query}",
 53 |             headers=HEADERS,
 54 |             timeout=30,
 55 |         )
 56 |         r.raise_for_status()
 57 |         total = r.json()["total_count"]
 58 |         print(f"✓ Test query successful: {test_query} -> {total} results")
 59 |         return True
 60 |     except Exception as e:
 61 |         print(f"✗ Test query failed: {e}")
 62 |         return False
 63 | 
 64 | 
 65 | def get_nondraft_counts_at_time(timestamp: dt.datetime) -> Dict[str, int]:
 66 |     """Get non-draft PR counts at a specific timestamp with proper error handling."""
 67 |     counts = {}
 68 |     created_before = format_github_date(timestamp)
 69 | 
 70 |     print(f"Querying for timestamp: {timestamp} (GitHub format: {created_before})")
 71 | 
 72 |     for query_base, key in NONDRAFT_QUERIES.items():
 73 |         # Add time filter - PRs created before this timestamp
 74 |         full_query = f"{query_base}+created:<{created_before}"
 75 | 
 76 |         print(f"  {key}: {full_query}")
 77 | 
 78 |         for attempt in range(3):  # Max 3 attempts
 79 |             try:
 80 |                 r = requests.get(
 81 |                     f"https://api.github.com/search/issues?q={full_query}",
 82 |                     headers=HEADERS,
 83 |                     timeout=30,
 84 |                 )
 85 | 
 86 |                 if r.status_code == 403:
 87 |                     wait_time = 10 * (2**attempt)  # 10s, 20s, 40s
 88 |                     print(f"    Rate limited, waiting {wait_time}s...")
 89 |                     time.sleep(wait_time)
 90 |                     continue
 91 |                 elif r.status_code == 422:
 92 |                     print(f"    Query syntax error for {key}, skipping")
 93 |                     counts[key] = 0
 94 |                     break
 95 | 
 96 |                 r.raise_for_status()
 97 |                 count = r.json()["total_count"]
 98 |                 counts[key] = count
 99 |                 print(f"    ✓ {key}: {count}")
100 |                 break
101 | 
102 |             except Exception as e:
103 |                 if attempt == 2:  # Last attempt
104 |                     print(f"    ✗ Failed {key}: {e}")
105 |                     counts[key] = 0
106 |                 else:
107 |                     print(f"    Retry {attempt + 1} for {key}")
108 | 
109 |         # No rate limiting needed with PAT (5000 requests/hour)
110 |         time.sleep(0.1)  # Just a tiny delay to be safe
111 | 
112 |     return counts
113 | 
114 | 
115 | def enforce_constraints(row: dict) -> dict:
116 |     """Enforce logical constraints: merged <= nondraft <= total for each agent."""
117 |     agents = ["copilot", "codex", "cursor", "devin", "codegen"]
118 | 
119 |     for agent in agents:
120 |         total_key = f"{agent}_total"
121 |         merged_key = f"{agent}_merged"
122 |         nondraft_key = f"{agent}_nondraft"
123 | 
124 |         if all(key in row for key in [total_key, merged_key, nondraft_key]):
125 |             total = int(row[total_key])
126 |             merged = int(row[merged_key])
127 |             nondraft = int(row[nondraft_key])
128 | 
129 |             # Enforce constraints: merged <= nondraft <= total
130 |             # Start from the bottom and work up
131 |             nondraft = max(
132 |                 merged, min(nondraft, total)
133 |             )  # nondraft between merged and total
134 | 
135 |             row[nondraft_key] = nondraft
136 | 
137 |     return row
138 | 
139 | 
140 | def main():
141 |     """Main function to process data with strategic sampling."""
142 |     input_file = Path("data.csv")
143 |     backup_file = Path("data_backup.csv")
144 | 
145 |     if not input_file.exists():
146 |         print("❌ Error: data.csv not found!")
147 |         return
148 | 
149 |     # Test API first
150 |     if not test_single_query():
151 |         print("❌ API test failed. Check your connection and try again.")
152 |         return
153 | 
154 |     # Create backup of original data
155 |     print(f"📋 Creating backup: {backup_file}")
156 |     import shutil
157 | 
158 |     shutil.copy2(input_file, backup_file)
159 |     print(f"✅ Backup created")
160 | 
161 |     print(f"📖 Reading {input_file}")
162 | 
163 |     # Read all data
164 |     with input_file.open("r", newline="") as f:
165 |         reader = csv.DictReader(f)
166 |         fieldnames = list(reader.fieldnames)
167 |         rows = list(reader)
168 | 
169 |     total_rows = len(rows)
170 |     print(f"📊 Found {total_rows} rows")
171 | 
172 |     # New fieldnames with non-draft columns (only add if not already present)
173 |     nondraft_columns = list(NONDRAFT_QUERIES.values())
174 |     missing_columns = [col for col in nondraft_columns if col not in fieldnames]
175 |     new_fieldnames = fieldnames + missing_columns
176 | 
177 |     if missing_columns:
178 |         print(f"📋 Adding columns: {missing_columns}")
179 |     else:
180 |         print("📋 All nondraft columns already present")
181 | 
182 |     print(f"🎯 Getting exact data for all {total_rows} timestamps")
183 |     print("📡 This will query GitHub API for each timestamp - may take a while")
184 | 
185 |     # Process all rows with exact data - write to temp file first
186 |     temp_file = input_file.with_suffix(".tmp")
187 |     with temp_file.open("w", newline="") as f:
188 |         writer = csv.DictWriter(f, fieldnames=new_fieldnames)
189 |         writer.writeheader()
190 | 
191 |         for idx in range(total_rows):
192 |             row = rows[idx].copy()
193 |             timestamp = parse_timestamp(row["timestamp"])
194 | 
195 |             print(f"\n📡 Row {idx+1}/{total_rows}: {row['timestamp']}")
196 | 
197 |             try:
198 |                 counts = get_nondraft_counts_at_time(timestamp)
199 |                 row.update(counts)
200 | 
201 |                 # Enforce constraints to ensure data integrity
202 |                 row = enforce_constraints(row)
203 | 
204 |                 print(f"✅ Data retrieved and validated")
205 |             except Exception as e:
206 |                 print(f"❌ Failed: {e}")
207 |                 # Use zeros for failed queries
208 |                 for key in NONDRAFT_QUERIES.values():
209 |                     row[key] = 0
210 | 
211 |             writer.writerow(row)
212 | 
213 |             # No rate limiting needed between rows with PAT
214 |             if idx < total_rows - 1:
215 |                 print("⏱️  Brief pause...")
216 |                 time.sleep(0.2)
217 | 
218 |     # Replace original file with temp file
219 |     temp_file.replace(input_file)
220 | 
221 |     print(f"\n✅ Complete! Updated {input_file} with exact non-draft data")
222 |     print(f"📈 Queried {total_rows} exact timestamps")
223 |     print(f"💾 Original data backed up to {backup_file}")
224 | 
225 |     # Show sample of results
226 |     print(f"\n📋 Sample results:")
227 |     with input_file.open("r") as f:
228 |         reader = csv.DictReader(f)
229 |         for i, row in enumerate(reader):
230 |             if i < 3:  # Show first 3 rows
231 |                 print(
232 |                     f"  Row {i+1}: copilot_nondraft={row['copilot_nondraft']}, codex_nondraft={row['codex_nondraft']}"
233 |                 )
234 | 
235 | 
236 | if __name__ == "__main__":
237 |     print("🚀 Adding non-draft PR data to data.csv")
238 |     print("This will query GitHub API for exact data at each timestamp.")
239 | 
240 |     response = input("\nContinue? (y/N): ")
241 |     if response.lower() == "y":
242 |         main()
243 |     else:
244 |         print("Cancelled.")
245 | 


--------------------------------------------------------------------------------
/scripts/reconcile_codegen_merged.py:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env python3
  2 | """
  3 | ONE-TIME SCRIPT: Reconcile merged PR counts for codegen specifically using GitHub API.
  4 | Used to fix discrepancies in codegen merged data after data collection issues.
  5 | Updates only the codegen_merged column for rows where codegen data exists (starting from row 122).
  6 | Completed: Fixed codegen merged counts to match actual GitHub data.
  7 | """
  8 | 
  9 | import csv
 10 | import datetime as dt
 11 | import time
 12 | import os
 13 | from pathlib import Path
 14 | import requests
 15 | import shutil
 16 | from typing import Dict
 17 | 
 18 | # GitHub API headers - include PAT if available
 19 | HEADERS = {"Accept": "application/vnd.github+json", "User-Agent": "PR-Watcher"}
 20 | if github_token := os.getenv("GITHUB_TOKEN"):
 21 |     HEADERS["Authorization"] = f"token {github_token}"
 22 |     print("✅ Using GitHub PAT for authentication")
 23 | else:
 24 |     print("⚠️  No GitHub PAT found, using unauthenticated requests")
 25 | 
 26 | # Query for merged codegen PRs
 27 | MERGED_QUERY = "is:pr+author:codegen-sh[bot]+is:merged"
 28 | 
 29 | 
 30 | def parse_timestamp(timestamp_str: str) -> dt.datetime:
 31 |     """Parse CSV timestamp to datetime object."""
 32 |     return dt.datetime.strptime(timestamp_str.replace("‑", "-"), "%Y-%m-%d %H:%M:%S")
 33 | 
 34 | 
 35 | def format_github_date(timestamp: dt.datetime) -> str:
 36 |     """Format datetime for GitHub API (ISO format with UTC)."""
 37 |     return timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
 38 | 
 39 | 
 40 | def get_merged_count(timestamp: dt.datetime) -> int:
 41 |     """Get count of merged PRs for codegen up to the given timestamp."""
 42 |     github_time = format_github_date(timestamp)
 43 |     query = f"{MERGED_QUERY}+created:<{github_time}"
 44 | 
 45 |     try:
 46 |         response = requests.get(
 47 |             f"https://api.github.com/search/issues?q={query}",
 48 |             headers=HEADERS,
 49 |             timeout=30,
 50 |         )
 51 | 
 52 |         if response.status_code == 200:
 53 |             data = response.json()
 54 |             return data.get("total_count", 0)
 55 |         elif response.status_code == 403:
 56 |             print(f"  Rate limited, waiting 20 seconds...")
 57 |             time.sleep(20)
 58 |             return get_merged_count(timestamp)  # Retry
 59 |         else:
 60 |             print(f"  Error {response.status_code}: {response.text}")
 61 |             return None
 62 | 
 63 |     except Exception as e:
 64 |         print(f"  Request failed: {e}")
 65 |         return None
 66 | 
 67 | 
 68 | def create_backup():
 69 |     """Create a backup of the current data.csv"""
 70 |     backup_path = Path("data_merged_backup.csv")
 71 |     shutil.copy2("data.csv", backup_path)
 72 |     print(f"✅ Backup created: {backup_path}")
 73 | 
 74 | 
 75 | def main():
 76 |     print("🔄 Starting codegen merged PR reconciliation...")
 77 | 
 78 |     # Create backup
 79 |     create_backup()
 80 | 
 81 |     # Read the current data
 82 |     rows = []
 83 |     with open("data.csv", "r", encoding="utf-8") as f:
 84 |         reader = csv.reader(f)
 85 |         header = next(reader)
 86 |         rows = list(reader)
 87 | 
 88 |     print(f"📊 Total rows: {len(rows)}")
 89 | 
 90 |     # Find codegen_merged column index
 91 |     try:
 92 |         codegen_merged_idx = header.index("codegen_merged")
 93 |         codegen_total_idx = header.index("codegen_total")
 94 |         timestamp_idx = header.index("timestamp")
 95 |     except ValueError as e:
 96 |         print(f"❌ Column not found: {e}")
 97 |         return
 98 | 
 99 |     # Find rows where codegen data exists (codegen_total > 0)
100 |     codegen_rows = []
101 |     for i, row in enumerate(rows):
102 |         if int(row[codegen_total_idx]) > 0:
103 |             codegen_rows.append(i)
104 | 
105 |     print(
106 |         f"🎯 Found {len(codegen_rows)} rows with codegen data (starting from row {codegen_rows[0] + 2})"
107 |     )
108 | 
109 |     # Track differences
110 |     differences = []
111 | 
112 |     # Process each row with codegen data
113 |     for i, row_idx in enumerate(codegen_rows):
114 |         row = rows[row_idx]
115 |         timestamp_str = row[timestamp_idx]
116 |         old_merged = int(row[codegen_merged_idx])
117 | 
118 |         print(
119 |             f"Processing {i+1}/{len(codegen_rows)}: {timestamp_str} (old merged: {old_merged})",
120 |             end="",
121 |         )
122 | 
123 |         try:
124 |             timestamp = parse_timestamp(timestamp_str)
125 |             new_merged = get_merged_count(timestamp)
126 | 
127 |             if new_merged is not None:
128 |                 if new_merged != old_merged:
129 |                     differences.append(
130 |                         {
131 |                             "row": row_idx + 2,  # +2 for header and 0-indexing
132 |                             "timestamp": timestamp_str,
133 |                             "old_merged": old_merged,
134 |                             "new_merged": new_merged,
135 |                             "difference": new_merged - old_merged,
136 |                         }
137 |                     )
138 |                     print(f" → {new_merged} (diff: {new_merged - old_merged:+d})")
139 |                 else:
140 |                     print(f" → {new_merged} (no change)")
141 | 
142 |                 # Update the row
143 |                 row[codegen_merged_idx] = str(new_merged)
144 |             else:
145 |                 print(" → API error, skipping")
146 | 
147 |         except Exception as e:
148 |             print(f" → Error: {e}")
149 | 
150 |         # Rate limiting delay
151 |         time.sleep(1)
152 | 
153 |     # Write updated data back
154 |     with open("data.csv", "w", newline="", encoding="utf-8") as f:
155 |         writer = csv.writer(f)
156 |         writer.writerow(header)
157 |         writer.writerows(rows)
158 | 
159 |     print(f"\n✅ Reconciliation complete!")
160 |     print(f"📈 Updated {len(codegen_rows)} codegen rows")
161 | 
162 |     # Report differences
163 |     if differences:
164 |         print(f"\n📊 Found {len(differences)} differences:")
165 |         for diff in differences:
166 |             print(f"  Row {diff['row']}: {diff['timestamp']}")
167 |             print(
168 |                 f"    Old: {diff['old_merged']}, New: {diff['new_merged']}, Diff: {diff['difference']:+d}"
169 |             )
170 |     else:
171 |         print("\n✅ No differences found - all merged counts were already accurate!")
172 | 
173 | 
174 | if __name__ == "__main__":
175 |     main()
176 | 


--------------------------------------------------------------------------------
/templates/index_template.html:
--------------------------------------------------------------------------------
  1 | <!DOCTYPE html>
  2 | <html lang="en">
  3 |   <head>
  4 |     <meta charset="UTF-8" />
  5 |     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6 |     <title>PR Arena - AI Coding Agent Leaderboard</title>
  7 |     <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📊</text></svg>">
  8 |     <link rel="stylesheet" href="styles.css" />
  9 |     <meta http-equiv="refresh" content="3600" />
 10 |     <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
 11 |     <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
 12 |     <link rel="preconnect" href="https://fonts.googleapis.com" />
 13 |     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
 14 |     <link
 15 |       href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;1,400&family=IBM+Plex+Mono:wght@300;400;500;600&display=swap"
 16 |       rel="stylesheet"
 17 |     />
 18 | 
 19 |     <!-- Microsoft Clarity Analytics -->
 20 |     <script type="text/javascript">
 21 |         (function(c,l,a,r,i,t,y){
 22 |             c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
 23 |             t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
 24 |             y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
 25 |         })(window, document, "clarity", "script", "s40u7bxxnn");
 26 |     </script>
 27 |   </head>
 28 |   <body>
 29 |     <div class="container">
 30 |       <header class="hero">
 31 |         <h1>PR Arena</h1>
 32 |         <p>Software engineering agents head to head</p>
 33 |       </header>
 34 | 
 35 |       <!-- Dynamic Leaderboard -->
 36 |       <section class="leaderboard">
 37 |         <div class="leaderboard-header">
 38 |           <h2>Leaderboard</h2>
 39 |           <div class="sort-controls">
 40 |             <div class="dropdown-container">
 41 |               <button class="sort-btn dropdown-btn" data-sort="success-rate" id="successRateBtn">
 42 |                 Success Rate <span class="mode-indicator">(M/R)</span> <span class="dropdown-arrow">▼</span>
 43 |               </button>
 44 |               <div class="dropdown-menu" id="successRateDropdown">
 45 |                 <button class="dropdown-item active" data-rate-type="ready">Merged/Ready PRs</button>
 46 |                 <button class="dropdown-item" data-rate-type="total">Merged/All PRs, including drafts</button>
 47 |               </div>
 48 |             </div>
 49 |             <button class="sort-btn active" data-sort="total-prs">Total PRs</button>
 50 |             <button class="sort-btn" data-sort="merged-prs">Merged PRs</button>
 51 |           </div>
 52 |         </div>
 53 | 
 54 |         <div class="agents-container" id="agentsContainer">
 55 | {% for agent in agents %}
 56 |           <div class="agent"
 57 |                data-success-rate="{{ stats[agent.key].rate }}"
 58 |                data-success-rate-ready="{{ stats[agent.key].ready_rate }}"
 59 |                data-success-rate-total="{{ stats[agent.key].total_rate }}"
 60 |                data-total-prs="{{ stats[agent.key].total }}"
 61 |                data-ready-prs="{{ stats[agent.key].nondraft }}"
 62 |                data-merged-prs="{{ stats[agent.key].merged }}"
 63 |                data-agent-name="{{ agent.long_name }}">
 64 |             <div class="agent-header">
 65 |               <div class="rank" data-rank="{{ loop.index }}">#{{ loop.index }}</div>
 66 |               <div class="agent-dot" style="background-color: {{ agent.color }}"></div>
 67 |               <div class="agent-info">
 68 |                 <h3><a href="{{ agent.info_url }}" target="_blank">{{ agent.long_name }}</a></h3>
 69 |                 <span class="agent-subtitle">
 70 |                   <span class="pr-count">{{ stats[agent.key].nondraft | comma }}</span>
 71 |                   <span class="pr-type">ready</span> /
 72 |                   <span class="pr-total">{{ stats[agent.key].total | comma }}</span>
 73 |                   <span class="pr-total-label">all PRs</span>
 74 |                 </span>
 75 |               </div>
 76 |             </div>
 77 |             <div class="metrics">
 78 |               <div class="primary-metric">
 79 |                 <span class="metric-value">
 80 |                   {{ stats[agent.key].rate | round(1) }}%
 81 |                 </span>
 82 |                 <span class="metric-label">Success Rate</span>
 83 |               </div>
 84 |               <div class="secondary-metrics">
 85 |                 <div class="metric draft-metric">
 86 |                   <a href="{{ agent.draft_query_url }}" target="_blank">
 87 |                     <span class="metric-count">{{ (stats[agent.key].total - stats[agent.key].nondraft) | comma }}</span>
 88 |                     <span>draft</span>
 89 |                   </a>
 90 |                 </div>
 91 |                 <div class="metric">
 92 |                   <a href="{{ agent.ready_query_url }}" target="_blank">
 93 |                     <span class="metric-count">{{ stats[agent.key].nondraft | comma }}</span>
 94 |                     <span>ready</span>
 95 |                   </a>
 96 |                 </div>
 97 |                 <div class="metric">
 98 |                   <a href="{{ agent.merged_query_url }}" target="_blank">
 99 |                     <span class="metric-count">{{ stats[agent.key].merged | comma }}</span>
100 |                     <span>merged</span>
101 |                   </a>
102 |                 </div>
103 |               </div>
104 |             </div>
105 |           <div class="progress-bar">
106 |             <div
107 |               class="progress"
108 |               style="width: {{ stats[agent.key].rate }}%; background-color: {{ agent.color }}"
109 |             ></div>
110 |           </div>
111 |         </div>
112 |         {% endfor %}
113 |       </section>
114 | 
115 |       <!-- Definitions section -->
116 |       <section class="definitions">
117 |         <div class="explanation-section">
118 |           <button class="explanation-toggle" id="explanationToggle">
119 |             <span class="toggle-text">IMPORTANT DEFINITIONS</span>
120 |             <span class="toggle-arrow">▼</span>
121 |           </button>
122 |           <div class="explanation-content" id="explanationContent">
123 |             <p>Different AI coding agents follow different workflows when creating pull requests:</p>
124 |             <ul>
125 |               <li><strong>All PRs:</strong> Every pull request created by an agent, including DRAFT PRs.</li>
126 |               <li><strong>Ready PRs:</strong> Non-draft pull requests that are ready for review and merging</li>
127 |               <li><strong>Merged PRs:</strong> Pull requests that were successfully merged into the codebase</li>
128 |             </ul>
129 |             <p><strong>Key workflow differences:</strong> Some agents like <strong>Codex</strong> iterate privately and create ready PRs directly, resulting in very few drafts but high merge rates. Others like <strong>Copilot</strong> and <strong>Codegen</strong> create draft PRs first, encouraging public iteration before marking them ready for review.</p>
130 |             <p>By default, we show success rates using <strong>Ready PRs only</strong> to fairly compare agents across different workflows. This focuses on each agent's ability to produce mergeable code, regardless of whether they iterate publicly (with drafts) or privately. Toggle to "Include draft PRs" to see the complete picture of all activity.</p>
131 |           </div>
132 |         </div>
133 |       </section>
134 | 
135 |       <!-- Historic Chart -->
136 |       <section class="chart">
137 |         <h2>PR Volume & Success Rate</h2>
138 |         <div class="chart-controls">
139 |           <div class="control-row">
140 |             <div class="control-section">
141 |               <span class="control-label">AGENTS</span>
142 |               <div class="toggle-buttons">
143 |                 {% for agent in agents %}
144 |                 <button
145 |                   id="toggle{{ agent.key|title }}"
146 |                   class="toggle-btn active"
147 |                   data-agent="{{ agent.key }}"
148 |                 >
149 |                   <span
150 |                     class="toggle-icon"
151 |                     style="background-color: {{ agent.color }}"
152 |                   ></span
153 |                   >{{ agent.long_name }}
154 |                 </button>
155 |                 {% endfor %}
156 |               </div>
157 |             </div>
158 |             <div class="control-section">
159 |               <span class="control-label">SUCCESS RATE</span>
160 |               <div class="toggle-buttons">
161 |                 <button id="rateReady" class="rate-btn active" data-rate="ready">
162 |                   <span class="rate-icon">●</span>Ready PRs Only (M/R)
163 |                 </button>
164 |                 <button id="rateTotal" class="rate-btn" data-rate="total">
165 |                   All PRs (M/A)
166 |                 </button>
167 |               </div>
168 |             </div>
169 |             <div class="control-section">
170 |               <span class="control-label">VIEW MODE</span>
171 |               <div class="toggle-buttons">
172 |                 <button id="viewAll" class="view-btn active" data-view="all">
173 |                   <span class="view-icon">●</span>Complete View
174 |                 </button>
175 |                 <button id="viewBarsOnly" class="view-btn" data-view="bars">
176 |                   Volume Only
177 |                 </button>
178 |                 <button id="viewLinesOnly" class="view-btn" data-view="lines">
179 |                   Success Rate Only
180 |                 </button>
181 |               </div>
182 |             </div>
183 |           </div>
184 |         </div>
185 |         <div class="chart-container">
186 |           <canvas id="prChart" width="800" height="400"></canvas>
187 |         </div>
188 |       </section>
189 | 
190 |       <!-- Simple Footer -->
191 |       <footer class="footer">
192 |         <p>
193 |           Updated {{ timestamp }} •
194 |           <a href="https://github.com/aavetis/ai-pr-watcher" target="_blank">by aavetis</a>
195 |         </p>
196 |       </footer>
197 |     </div>
198 | 
199 |     <script>
200 |       // Chart instance
201 |       let chartInstance = null;
202 |       let currentRateType = 'ready'; // Track current success rate type
203 |       let currentChartRateType = 'ready'; // Track current chart success rate type
204 | 
205 |       document.addEventListener("DOMContentLoaded", function () {
206 |         initializeSorting();
207 |         initializeDropdown();
208 |         initializeExplanation();
209 |         loadCharts();
210 |         // Sort by total PRs on page load
211 |         sortAgents("total-prs");
212 |       });
213 | 
214 |       // Dynamic Sorting
215 |       function initializeSorting() {
216 |         const sortButtons = document.querySelectorAll(".sort-btn");
217 |         sortButtons.forEach((btn) => {
218 |           if (!btn.classList.contains('dropdown-btn')) {
219 |             btn.addEventListener("click", () => {
220 |               // Update active button
221 |               sortButtons.forEach((b) => b.classList.remove("active"));
222 |               btn.classList.add("active");
223 | 
224 |               // Sort agents
225 |               sortAgents(btn.dataset.sort);
226 |             });
227 |           }
228 |         });
229 |       }
230 | 
231 |       function initializeDropdown() {
232 |         const dropdownBtn = document.getElementById('successRateBtn');
233 |         const dropdown = document.getElementById('successRateDropdown');
234 |         const dropdownItems = document.querySelectorAll('.dropdown-item');
235 |         const arrow = dropdownBtn.querySelector('.dropdown-arrow');
236 | 
237 |         // Toggle dropdown
238 |         dropdownBtn.addEventListener('click', (e) => {
239 |           e.stopPropagation();
240 |           const isShowing = dropdown.classList.contains('show');
241 |           dropdown.classList.toggle('show');
242 | 
243 |           // Rotate arrow
244 |           if (dropdown.classList.contains('show')) {
245 |             arrow.style.transform = 'rotate(180deg)';
246 |           } else {
247 |             arrow.style.transform = 'rotate(0deg)';
248 |           }
249 |         });
250 | 
251 |         // Close dropdown when clicking outside
252 |         document.addEventListener('click', () => {
253 |           dropdown.classList.remove('show');
254 |           arrow.style.transform = 'rotate(0deg)';
255 |         });
256 | 
257 |         // Handle dropdown item selection
258 |         dropdownItems.forEach(item => {
259 |           item.addEventListener('click', (e) => {
260 |             e.stopPropagation();
261 | 
262 |             // Update active item
263 |             dropdownItems.forEach(i => i.classList.remove('active'));
264 |             item.classList.add('active');
265 | 
266 |             // Update rate type
267 |             currentRateType = item.dataset.rateType;
268 | 
269 |             // Update button text to show current mode
270 |             const modeText = currentRateType === 'ready' ? '(M/R)' : '(M/A)';
271 |             dropdownBtn.innerHTML = `Success Rate <span class="mode-indicator">${modeText}</span> <span class="dropdown-arrow">▼</span>`;
272 | 
273 |             // Update all agents' data and resort
274 |             updateAgentMetrics(currentRateType);
275 |             sortAgents('success-rate');
276 | 
277 |             // Close dropdown
278 |             dropdown.classList.remove('show');
279 |             arrow.style.transform = 'rotate(0deg)';
280 | 
281 |             // Make sure success rate button is active
282 |             document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
283 |             dropdownBtn.classList.add('active');
284 |           });
285 |         });
286 |       }
287 | 
288 |       function initializeExplanation() {
289 |         const toggle = document.getElementById('explanationToggle');
290 |         const content = document.getElementById('explanationContent');
291 |         const arrow = toggle.querySelector('.toggle-arrow');
292 | 
293 |         toggle.addEventListener('click', () => {
294 |           const isExpanded = content.classList.contains('expanded');
295 | 
296 |           if (isExpanded) {
297 |             content.classList.remove('expanded');
298 |             toggle.classList.remove('expanded');
299 |           } else {
300 |             content.classList.add('expanded');
301 |             toggle.classList.add('expanded');
302 |           }
303 |         });
304 |       }
305 | 
306 |       function updateAgentMetrics(rateType) {
307 |         const agents = document.querySelectorAll('.agent');
308 | 
309 |         agents.forEach(agent => {
310 |           const rate = rateType === 'ready'
311 |             ? parseFloat(agent.dataset.successRateReady)
312 |             : parseFloat(agent.dataset.successRateTotal);
313 | 
314 |           // Update the data attribute for sorting
315 |           agent.dataset.successRate = rate;
316 | 
317 |           // Update the primary metric display
318 |           const metricValue = agent.querySelector('.metric-value');
319 |           metricValue.textContent = rate.toFixed(1) + '%';
320 | 
321 |           // Update the progress bar
322 |           const progressBar = agent.querySelector('.progress');
323 |           progressBar.style.width = rate + '%';
324 | 
325 |           // Update subtitle to highlight the current mode
326 |           const subtitle = agent.querySelector('.agent-subtitle');
327 |           const prCount = subtitle.querySelector('.pr-count');
328 |           const prType = subtitle.querySelector('.pr-type');
329 | 
330 |           if (rateType === 'ready') {
331 |             prCount.textContent = parseInt(agent.dataset.readyPrs).toLocaleString();
332 |             prType.textContent = 'ready';
333 |             prType.style.fontWeight = '600';
334 |             subtitle.querySelector('.pr-total-label').style.opacity = '0.6';
335 |           } else {
336 |             prCount.textContent = parseInt(agent.dataset.totalPrs).toLocaleString();
337 |             prType.textContent = 'all';
338 |             prType.style.fontWeight = '600';
339 |             subtitle.querySelector('.pr-total-label').style.opacity = '1';
340 |           }
341 |         });
342 |       }
343 | 
344 |       function sortAgents(criteria) {
345 |         const container = document.getElementById("agentsContainer");
346 |         const agents = Array.from(container.children);
347 | 
348 |         agents.sort((a, b) => {
349 |           let valueA, valueB;
350 | 
351 |           switch (criteria) {
352 |             case "success-rate":
353 |               valueA = parseFloat(a.dataset.successRate);
354 |               valueB = parseFloat(b.dataset.successRate);
355 |               break;
356 |             case "total-prs":
357 |               valueA = parseInt(a.dataset.totalPrs);
358 |               valueB = parseInt(b.dataset.totalPrs);
359 |               break;
360 |             case "merged-prs":
361 |               valueA = parseInt(a.dataset.mergedPrs);
362 |               valueB = parseInt(b.dataset.mergedPrs);
363 |               break;
364 |             default:
365 |               return 0;
366 |           }
367 | 
368 |           return valueB - valueA; // Descending order
369 |         });
370 | 
371 |         // Update ranks and re-append in new order
372 |         agents.forEach((agent, index) => {
373 |           const rank = agent.querySelector(".rank");
374 |           rank.textContent = `#${index + 1}`;
375 |           rank.dataset.rank = index + 1;
376 |           container.appendChild(agent);
377 |         });
378 | 
379 |         // Add animation
380 |         agents.forEach((agent, index) => {
381 |           agent.style.animation = "none";
382 |           setTimeout(() => {
383 |             agent.style.animation = `fadeInUp 0.4s ease forwards`;
384 |             agent.style.animationDelay = `${index * 0.1}s`;
385 |           }, 10);
386 |         });
387 |       }
388 | 
389 |       // Chart Loading
390 |       async function loadCharts() {
391 |         try {
392 |           console.log("Loading chart data...");
393 |           const response = await fetch("chart-data.json");
394 |           if (!response.ok) {
395 |             throw new Error(`HTTP error! status: ${response.status}`);
396 |           }
397 |           const data = await response.json();
398 |           console.log("Chart data loaded:", data);
399 | 
400 |           initializeChart(data);
401 |         } catch (error) {
402 |           console.error("Charts failed to load:", error);
403 |           // Show error message in chart
404 |           document.querySelector(".chart-container").innerHTML =
405 |             '<p style="color: #ef4444; text-align: center; padding: 2rem;">Failed to load chart</p>';
406 |         }
407 |       }
408 | 
409 |       function initializeChart(chartData) {
410 |         console.log("Initializing combined chart...");
411 |         const ctx = document.getElementById("prChart").getContext("2d");
412 | 
413 |         chartInstance = new Chart(ctx, {
414 |           type: "bar",
415 |           data: chartData,
416 |           options: {
417 |             responsive: true,
418 |             maintainAspectRatio: false,
419 |             interaction: {
420 |               intersect: false,
421 |               mode: "point",
422 |             },
423 |             plugins: {
424 |               title: {
425 |                 display: false,
426 |               },
427 |               legend: {
428 |                 display: true,
429 |                 position: "right",
430 |                 labels: {
431 |                   usePointStyle: true,
432 |                   pointStyle: "circle",
433 |                   color: "#64748b",
434 |                   font: {
435 |                     family: "IBM Plex Mono",
436 |                     size: 11,
437 |                     weight: "500",
438 |                   },
439 |                   padding: 15,
440 |                   boxWidth: 12,
441 |                 },
442 |               },
443 |               tooltip: {
444 |                 titleColor: "#1f2937",
445 |                 bodyColor: "#374151",
446 |                 backgroundColor: "rgba(255, 255, 255, 0.95)",
447 |                 borderColor: "#e5e7eb",
448 |                 borderWidth: 1,
449 |                 callbacks: {
450 |                   title: function (context) {
451 |                     return context[0].label;
452 |                   },
453 |                   label: function (context) {
454 |                     const datasetLabel = context.dataset.label;
455 |                     const value = context.parsed.y;
456 | 
457 |                     if (datasetLabel.includes("Success %")) {
458 |                       return datasetLabel + ": " + value.toFixed(1) + "%";
459 |                     } else {
460 |                       return (
461 |                         datasetLabel + ": " + value.toLocaleString() + " PRs"
462 |                       );
463 |                     }
464 |                   },
465 |                 },
466 |               },
467 |             },
468 |             scales: {
469 |               x: {
470 |                 title: {
471 |                   display: true,
472 |                   text: "Time",
473 |                   color: "#475569",
474 |                   font: {
475 |                     family: "IBM Plex Mono",
476 |                     size: 12,
477 |                     weight: "500",
478 |                   },
479 |                 },
480 |                 grid: { color: "#f1f5f9", lineWidth: 1 },
481 |                 ticks: {
482 |                   color: "#475569",
483 |                   font: { family: "IBM Plex Mono", size: 11 },
484 |                   maxRotation: 45,
485 |                   minRotation: 0
486 |                 },
487 |               },
488 |               y: {
489 |                 type: "linear",
490 |                 display: true,
491 |                 position: "left",
492 |                 title: {
493 |                   display: true,
494 |                   text: "Number of PRs",
495 |                   color: "#475569",
496 |                   font: {
497 |                     family: "IBM Plex Mono",
498 |                     size: 12,
499 |                     weight: "500",
500 |                   },
501 |                 },
502 |                 grid: { color: "#f1f5f9", lineWidth: 1 },
503 |                 ticks: {
504 |                   color: "#475569",
505 |                   font: { family: "IBM Plex Mono", size: 11 },
506 |                   callback: function(value) {
507 |                     return value.toLocaleString();
508 |                   }
509 |                 },
510 |                 beginAtZero: true,
511 |               },
512 |               y1: {
513 |                 type: "linear",
514 |                 display: true,
515 |                 position: "right",
516 |                 title: {
517 |                   display: true,
518 |                   text: "Success Rate (%)",
519 |                   color: "#475569",
520 |                   font: {
521 |                     family: "IBM Plex Mono",
522 |                     size: 12,
523 |                     weight: "500",
524 |                   },
525 |                 },
526 |                 ticks: {
527 |                   color: "#475569",
528 |                   font: { family: "IBM Plex Mono", size: 11 },
529 |                   callback: function (value) {
530 |                     return value + "%";
531 |                   },
532 |                 },
533 |                 beginAtZero: true,
534 |                 max: 100,
535 |                 grid: {
536 |                   drawOnChartArea: false,
537 |                 },
538 |               },
539 |             },
540 |           },
541 |         });
542 | 
543 |         console.log("Chart initialized");
544 | 
545 |         // Set up toggle functionality
546 |         setupToggleButtons();
547 | 
548 |         // Initialize chart with proper dataset visibility
549 |         updateChartVisibility();
550 |       }
551 | 
552 |       function updateChartVisibility() {
553 |         if (!chartInstance) return;
554 | 
555 |         chartInstance.data.datasets.forEach((dataset) => {
556 |           const isLine = dataset.type === "line";
557 |           const isBar = dataset.type === "bar";
558 |           const labelLower = dataset.label.toLowerCase();
559 | 
560 |           // Get the agent name from the dataset label
561 |           const agentMatch = labelLower.match(/(copilot|codex|cursor|devin|codegen)/);
562 |           const agent = agentMatch ? agentMatch[1] : null;
563 | 
564 |           // Check if agent is currently enabled
565 |           const agentButton = document.querySelector(`.toggle-btn[data-agent="${agent}"]`);
566 |           const agentEnabled = agentButton && agentButton.classList.contains("active");
567 | 
568 |           // Check current view mode
569 |           const activeViewBtn = document.querySelector(".view-btn.active");
570 |           const currentView = activeViewBtn ? activeViewBtn.dataset.view : "all";
571 | 
572 |           // For lines, check rate type
573 |           const isCorrectRateType = isLine ?
574 |             (currentChartRateType === 'ready' ? labelLower.includes('ready') : labelLower.includes('all')) :
575 |             true;
576 | 
577 |           // Set visibility based on all conditions
578 |           switch (currentView) {
579 |             case "all":
580 |               dataset.hidden = !agentEnabled || (isLine && !isCorrectRateType);
581 |               break;
582 |             case "bars":
583 |               dataset.hidden = !agentEnabled || isLine;
584 |               break;
585 |             case "lines":
586 |               dataset.hidden = !agentEnabled || isBar || (isLine && !isCorrectRateType);
587 |               break;
588 |           }
589 |         });
590 | 
591 |         chartInstance.update();
592 |       }
593 | 
594 |       function setupToggleButtons() {
595 |         const toggleButtons = document.querySelectorAll(".toggle-btn");
596 |         const viewButtons = document.querySelectorAll(".view-btn");
597 |         const rateButtons = document.querySelectorAll(".rate-btn");
598 | 
599 |         // Agent toggle functionality
600 |         toggleButtons.forEach((button) => {
601 |           button.addEventListener("click", function () {
602 |             const agent = this.dataset.agent;
603 | 
604 |             // Toggle button state
605 |             this.classList.toggle("active");
606 | 
607 |             // Update chart visibility
608 |             updateChartVisibility();
609 |           });
610 |         });
611 | 
612 |         // Success rate type functionality
613 |         rateButtons.forEach((button) => {
614 |           button.addEventListener("click", function () {
615 |             const rateType = this.dataset.rate;
616 | 
617 |             // Update button states
618 |             rateButtons.forEach((btn) => btn.classList.remove("active"));
619 |             this.classList.add("active");
620 | 
621 |             // Update current rate type
622 |             currentChartRateType = rateType;
623 | 
624 |             // Update chart visibility
625 |             updateChartVisibility();
626 |           });
627 |         });
628 | 
629 |         // View mode functionality
630 |         viewButtons.forEach((button) => {
631 |           button.addEventListener("click", function () {
632 |             const view = this.dataset.view;
633 | 
634 |             // Update button states
635 |             viewButtons.forEach((btn) => btn.classList.remove("active"));
636 |             this.classList.add("active");
637 | 
638 |             // Update chart visibility
639 |             updateChartVisibility();
640 |           });
641 |         });
642 |       }
643 |     </script>
644 |   </body>
645 | </html>
646 | 


--------------------------------------------------------------------------------
/templates/index_template_old.html:
--------------------------------------------------------------------------------
  1 | <!DOCTYPE html>
  2 | <html lang="en" data-theme="dark">
  3 |   <head>
  4 |     <meta charset="UTF-8" />
  5 |     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6 |     <title>AI Coding Agents</title>
  7 |     <link rel="stylesheet" href="styles.css" />
  8 |     <meta http-equiv="refresh" content="3600" />
  9 |     <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
 10 |     <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
 11 |     <link rel="preconnect" href="https://fonts.googleapis.com">
 12 |     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 13 |     <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
 14 |   </head>
 15 |   <body>
 16 |     <div class="container">
 17 |       <header class="hero">
 18 |         <h1>AI Coding Agents</h1>
 19 |         <p>Which AI writes the most successful code?</p>
 20 |       </header>
 21 |       <!-- Simple Leaderboard -->
 22 |       <section class="leaderboard">
 23 | {% for agent in agents %}
 24 |         <div class="agent" data-success-rate="{{ stats[agent.key].rate }}">
 25 |           <div class="agent-header">
 26 |             <div class="rank">#{{ loop.index }}</div>
 27 |             <div class="agent-dot" style="background-color: {{ agent.color }}"></div>
 28 |             <div class="agent-info">
 29 |               <h3><a href="{{ agent.info_url }}" target="_blank">{{ agent.long_name }}</a></h3>
 30 |               <span class="agent-subtitle">{{ stats[agent.key].total | comma }} PRs</span>
 31 |             </div>
 32 |           </div>
 33 |           <div class="metrics">
 34 |             <div class="primary-metric">
 35 |               <span class="metric-value">{{ stats[agent.key].rate | round(1) }}%</span>
 36 |               <span class="metric-label">Success Rate</span>
 37 |             </div>
 38 |             <div class="secondary-metrics">
 39 |               <div class="metric">
 40 |                 <a href="{{ agent.merged_query_url }}" target="_blank">{{ stats[agent.key].merged | comma }}</a>
 41 |                 <span>merged</span>
 42 |               </div>
 43 |             </div>
 44 |           </div>
 45 |           <div class="progress-bar">
 46 |             <div class="progress" style="width: {{ stats[agent.key].rate }}%; background-color: {{ agent.color }}"></div>
 47 |           </div>
 48 |         </div>
 49 | {% endfor %}
 50 |       </section>
 51 | 
 52 |       <!-- Simple Chart -->
 53 |       <section class="chart-section">
 54 |         <h2>Performance Over Time</h2>
 55 |         <div class="chart-container">
 56 |           <canvas id="mainChart"></canvas>
 57 |         </div>
 58 |       </section>
 59 | 
 60 |       <!-- Simple Footer -->
 61 |       <footer class="footer">
 62 |         <p>Updated {{ timestamp }} • <a href="https://github.com/aavetis/ai-pr-watcher">GitHub</a></p>
 63 |       </footer>
 64 |     </div>
 65 | 
 66 |     <script>
 67 |       // Simple chart initialization
 68 |       let chart = null;
 69 | 
 70 |       document.addEventListener('DOMContentLoaded', function() {
 71 |         loadChart();
 72 |       });
 73 | 
 74 |       async function loadChart() {
 75 |         try {
 76 |           const response = await fetch("chart-data.json");
 77 |           const data = await response.json();
 78 |           initChart(data);
 79 |         } catch (error) {
 80 |           console.error("Chart failed to load:", error);
 81 |         }
 82 |       }
 83 | 
 84 |       function initChart(data) {
 85 |         const ctx = document.getElementById('mainChart').getContext('2d');
 86 |         
 87 |         chart = new Chart(ctx, {
 88 |           type: 'line',
 89 |           data: data,
 90 |           options: {
 91 |             responsive: true,
 92 |             maintainAspectRatio: false,
 93 |             plugins: {
 94 |               legend: {
 95 |                 position: 'top',
 96 |                 labels: {
 97 |                   color: '#94a3b8',
 98 |                   font: { family: 'Inter', size: 12 },
 99 |                   padding: 20,
100 |                   usePointStyle: true
101 |                 }
102 |               }
103 |             },
104 |             scales: {
105 |               x: {
106 |                 type: 'time',
107 |                 time: { unit: 'day' },
108 |                 grid: { color: '#334155' },
109 |                 ticks: { color: '#94a3b8' }
110 |               },
111 |               y: {
112 |                 grid: { color: '#334155' },
113 |                 ticks: { color: '#94a3b8' }
114 |               }
115 |             }
116 |           }
117 |         });
118 |       }
119 |     </script>
120 |   </body>
121 | </html>
122 |       <section class="analytics-section">
123 |         <div class="section-header">
124 |           <h2 class="section-title">
125 |             <i class="fas fa-chart-line"></i>
126 |             Advanced Analytics
127 |           </h2>
128 |           <div class="time-range-selector">
129 |             <button class="time-btn active" data-range="7d">7D</button>
130 |             <button class="time-btn" data-range="30d">30D</button>
131 |             <button class="time-btn" data-range="90d">90D</button>
132 |             <button class="time-btn" data-range="all">ALL</button>
133 |           </div>
134 |         </div>
135 | 
136 |         <div class="analytics-grid">
137 |           <!-- Performance Overview Chart -->
138 |           <div class="chart-container large">
139 |             <div class="chart-header">
140 |               <h3>Performance Timeline</h3>
141 |               <div class="chart-controls">
142 |                 <button class="chart-toggle active" data-metric="volume">Volume</button>
143 |                 <button class="chart-toggle" data-metric="success-rate">Success Rate</button>
144 |                 <button class="chart-toggle" data-metric="velocity">Velocity</button>
145 |               </div>
146 |             </div>
147 |             <canvas id="performanceChart"></canvas>
148 |           </div>
149 | 
150 |           <!-- Activity Heatmap -->
151 |           <div class="chart-container medium">
152 |             <div class="chart-header">
153 |               <h3>Activity Heatmap</h3>
154 |               <p class="chart-subtitle">PR creation patterns by day & hour</p>
155 |             </div>
156 |             <div id="activityHeatmap" class="heatmap-container"></div>
157 |           </div>
158 | 
159 |           <!-- Language Distribution -->
160 |           <div class="chart-container medium">
161 |             <div class="chart-header">
162 |               <h3>Language Distribution</h3>
163 |               <p class="chart-subtitle">Most popular programming languages</p>
164 |             </div>
165 |             <canvas id="languageChart"></canvas>
166 |           </div>
167 | 
168 |           <!-- Competition Analysis -->
169 |           <div class="chart-container large">
170 |             <div class="chart-header">
171 |               <h3>Head-to-Head Competition</h3>
172 |               <div class="comparison-selectors">
173 |                 <select id="agent1Select" class="agent-selector">
174 |                   <option value="copilot">GitHub Copilot</option>
175 |                   <option value="codex">OpenAI Codex</option>
176 |                   <option value="cursor">Cursor Agents</option>
177 |                   <option value="devin">Devin</option>
178 |                   <option value="codegen">Codegen</option>
179 |                 </select>
180 |                 <span class="vs-indicator">VS</span>
181 |                 <select id="agent2Select" class="agent-selector">
182 |                   <option value="codex">OpenAI Codex</option>
183 |                   <option value="copilot">GitHub Copilot</option>
184 |                   <option value="cursor">Cursor Agents</option>
185 |                   <option value="devin">Devin</option>
186 |                   <option value="codegen">Codegen</option>
187 |                 </select>
188 |               </div>
189 |             </div>
190 |             <canvas id="comparisonChart"></canvas>
191 |           </div>
192 | 
193 |           <!-- Global Insights -->
194 |           <div class="insights-container">
195 |             <div class="insight-card">
196 |               <div class="insight-icon">
197 |                 <i class="fas fa-fire"></i>
198 |               </div>
199 |               <div class="insight-content">
200 |                 <h4>Hottest Agent</h4>
201 |                 <p>{{ hottest_agent.name }} with {{ hottest_agent.daily_prs }} PRs today</p>
202 |               </div>
203 |             </div>
204 | 
205 |             <div class="insight-card">
206 |               <div class="insight-icon">
207 |                 <i class="fas fa-trophy"></i>
208 |               </div>
209 |               <div class="insight-content">
210 |                 <h4>Quality Champion</h4>
211 |                 <p>{{ quality_champion.name }} leads with {{ quality_champion.rate }}% success rate</p>
212 |               </div>
213 |             </div>
214 | 
215 |             <div class="insight-card">
216 |               <div class="insight-icon">
217 |                 <i class="fas fa-rocket"></i>
218 |               </div>
219 |               <div class="insight-content">
220 |                 <h4>Speed Demon</h4>
221 |                 <p>{{ speed_demon.name }} averages {{ speed_demon.merge_time }} merge time</p>
222 |               </div>
223 |             </div>
224 | 
225 |             <div class="insight-card">
226 |               <div class="insight-icon">
227 |                 <i class="fas fa-globe"></i>
228 |               </div>
229 |               <div class="insight-content">
230 |                 <h4>Most Diverse</h4>
231 |                 <p>{{ most_diverse.name }} contributes to {{ most_diverse.repo_count }} repositories</p>
232 |               </div>
233 |             </div>
234 |           </div>
235 |         </div>
236 |       </section>
237 | 
238 |       <!-- Live Activity Feed -->
239 |       <section class="activity-section">
240 |         <div class="section-header">
241 |           <h2 class="section-title">
242 |             <i class="fas fa-rss"></i>
243 |             Live Activity Feed
244 |           </h2>
245 |           <div class="activity-controls">
246 |             <button class="filter-btn active" data-filter="all">All</button>
247 |             <button class="filter-btn" data-filter="merged">Merged</button>
248 |             <button class="filter-btn" data-filter="created">Created</button>
249 |           </div>
250 |         </div>
251 | 
252 |         <div class="activity-feed" id="activityFeed">
253 |           <!-- Activity items will be populated by JavaScript -->
254 |         </div>
255 |       </section>
256 | 
257 |       <footer class="footer">
258 |         <div class="footer-content">
259 |           <div class="footer-left">
260 |             <p class="footer-title">AI Coding Agents Arena</p>
261 |             <p class="footer-subtitle">Tracking the future of automated development</p>
262 |           </div>
263 |           <div class="footer-center">
264 |             <div class="footer-stat">
265 |               <span class="footer-stat-label">Last Updated</span>
266 |               <span class="footer-stat-value" id="last-updated">{{ timestamp }}</span>
267 |             </div>
268 |             <div class="footer-stat">
269 |               <span class="footer-stat-label">Data Source</span>
270 |               <span class="footer-stat-value">GitHub Public API</span>
271 |             </div>
272 |           </div>
273 |           <div class="footer-right">
274 |             <a href="https://github.com/aavetis/ai-pr-watcher" class="footer-link">
275 |               <i class="fab fa-github"></i>
276 |               View on GitHub
277 |             </a>
278 |             <div class="footer-social">
279 |               <a href="#" class="social-link"><i class="fab fa-twitter"></i></a>
280 |               <a href="#" class="social-link"><i class="fab fa-linkedin"></i></a>
281 |             </div>
282 |           </div>
283 |         </div>
284 |       </footer>
285 |     </div>
286 | 
287 |     <!-- Agent Detail Modal -->
288 |     <div id="agentModal" class="modal">
289 |       <div class="modal-content">
290 |         <div class="modal-header">
291 |           <h2 id="modalAgentName">Agent Details</h2>
292 |           <span class="modal-close" onclick="closeAgentModal()">&times;</span>
293 |         </div>
294 |         <div class="modal-body" id="modalBody">
295 |           <!-- Content populated by JavaScript -->
296 |         </div>
297 |       </div>
298 |     </div>
299 | 
300 |     <script>
301 |       // Global variables
302 |       let chartInstances = {};
303 |       let chartData = null;
304 |       let currentTheme = localStorage.getItem('theme') || 'dark';
305 | 
306 |       // Initialize the application
307 |       document.addEventListener('DOMContentLoaded', function() {
308 |         initializeTheme();
309 |         loadChartData();
310 |         initializeEventListeners();
311 |         startLiveUpdates();
312 |       });
313 | 
314 |       // Theme Management
315 |       function initializeTheme() {
316 |         document.documentElement.setAttribute('data-theme', currentTheme);
317 |         updateThemeIcon();
318 |       }
319 | 
320 |       function toggleTheme() {
321 |         currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
322 |         document.documentElement.setAttribute('data-theme', currentTheme);
323 |         localStorage.setItem('theme', currentTheme);
324 |         updateThemeIcon();
325 | 
326 |         // Recreate charts with new theme
327 |         Object.values(chartInstances).forEach(chart => chart.destroy());
328 |         chartInstances = {};
329 |         loadChartData();
330 |       }
331 | 
332 |       function updateThemeIcon() {
333 |         const icon = document.querySelector('.theme-toggle i');
334 |         icon.className = currentTheme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
335 |       }
336 | 
337 |       // Chart Data Loading
338 |       async function loadChartData() {
339 |         try {
340 |           const response = await fetch("chart-data.json");
341 |           chartData = await response.json();
342 |           initializeCharts();
343 |         } catch (error) {
344 |           console.error("Failed to load chart data:", error);
345 |           showFallback();
346 |         }
347 |       }
348 | 
349 |       function showFallback() {
350 |         // Show static chart fallback if needed
351 |         console.log("Showing chart fallback");
352 |       }
353 | 
354 |       // Chart Initialization
355 |       function initializeCharts() {
356 |         initializePerformanceChart();
357 |         initializeLanguageChart();
358 |         initializeComparisonChart();
359 |         initializeActivityHeatmap();
360 |       }
361 | 
362 |       function initializePerformanceChart() {
363 |         const ctx = document.getElementById('performanceChart').getContext('2d');
364 | 
365 |         const isDark = currentTheme === 'dark';
366 |         const textColor = isDark ? '#e2e8f0' : '#374151';
367 |         const gridColor = isDark ? '#374151' : '#e5e7eb';
368 | 
369 |         chartInstances.performance = new Chart(ctx, {
370 |           type: 'line',
371 |           data: chartData,
372 |           options: {
373 |             responsive: true,
374 |             maintainAspectRatio: false,
375 |             interaction: {
376 |               intersect: false,
377 |               mode: 'index'
378 |             },
379 |             plugins: {
380 |               legend: {
381 |                 position: 'top',
382 |                 labels: {
383 |                   color: textColor,
384 |                   usePointStyle: true,
385 |                   padding: 20
386 |                 }
387 |               },
388 |               tooltip: {
389 |                 backgroundColor: isDark ? '#1f2937' : '#ffffff',
390 |                 titleColor: textColor,
391 |                 bodyColor: textColor,
392 |                 borderColor: gridColor,
393 |                 borderWidth: 1
394 |               }
395 |             },
396 |             scales: {
397 |               x: {
398 |                 type: 'time',
399 |                 time: {
400 |                   unit: 'day'
401 |                 },
402 |                 grid: {
403 |                   color: gridColor
404 |                 },
405 |                 ticks: {
406 |                   color: textColor
407 |                 }
408 |               },
409 |               y: {
410 |                 grid: {
411 |                   color: gridColor
412 |                 },
413 |                 ticks: {
414 |                   color: textColor
415 |                 }
416 |               }
417 |             }
418 |           }
419 |         });
420 |       }
421 | 
422 |       function initializeLanguageChart() {
423 |         const ctx = document.getElementById('languageChart').getContext('2d');
424 | 
425 |         // Mock data for language distribution
426 |         const languageData = {
427 |           labels: ['JavaScript', 'Python', 'TypeScript', 'Java', 'Go', 'Rust', 'C++'],
428 |           datasets: [{
429 |             data: [35, 25, 15, 10, 8, 4, 3],
430 |             backgroundColor: [
431 |               '#f1c40f', '#3498db', '#2980b9', '#e74c3c',
432 |               '#00b4d8', '#e85d04', '#9b2226'
433 |             ],
434 |             borderWidth: 0
435 |           }]
436 |         };
437 | 
438 |         chartInstances.language = new Chart(ctx, {
439 |           type: 'doughnut',
440 |           data: languageData,
441 |           options: {
442 |             responsive: true,
443 |             maintainAspectRatio: false,
444 |             plugins: {
445 |               legend: {
446 |                 position: 'right',
447 |                 labels: {
448 |                   color: currentTheme === 'dark' ? '#e2e8f0' : '#374151',
449 |                   padding: 15
450 |                 }
451 |               }
452 |             }
453 |           }
454 |         });
455 |       }
456 | 
457 |       function initializeComparisonChart() {
458 |         const ctx = document.getElementById('comparisonChart').getContext('2d');
459 | 
460 |         // This will be populated based on agent selection
461 |         chartInstances.comparison = new Chart(ctx, {
462 |           type: 'radar',
463 |           data: {
464 |             labels: ['Total PRs', 'Success Rate', 'Velocity', 'Diversity', 'Quality'],
465 |             datasets: []
466 |           },
467 |           options: {
468 |             responsive: true,
469 |             maintainAspectRatio: false,
470 |             scales: {
471 |               r: {
472 |                 beginAtZero: true,
473 |                 grid: {
474 |                   color: currentTheme === 'dark' ? '#374151' : '#e5e7eb'
475 |                 },
476 |                 pointLabels: {
477 |                   color: currentTheme === 'dark' ? '#e2e8f0' : '#374151'
478 |                 }
479 |               }
480 |             },
481 |             plugins: {
482 |               legend: {
483 |                 labels: {
484 |                   color: currentTheme === 'dark' ? '#e2e8f0' : '#374151'
485 |                 }
486 |               }
487 |             }
488 |           }
489 |         });
490 |       }
491 | 
492 |       function initializeActivityHeatmap() {
493 |         // Create a simple heatmap using HTML/CSS
494 |         const container = document.getElementById('activityHeatmap');
495 |         container.innerHTML = generateHeatmapHTML();
496 |       }
497 | 
498 |       function generateHeatmapHTML() {
499 |         const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
500 |         const hours = Array.from({length: 24}, (_, i) => i);
501 | 
502 |         let html = '<div class="heatmap-grid">';
503 | 
504 |         // Generate mock activity data
505 |         days.forEach(day => {
506 |           html += `<div class="heatmap-row">`;
507 |           html += `<div class="day-label">${day}</div>`;
508 |           hours.forEach(hour => {
509 |             const intensity = Math.random();
510 |             const intensityClass = intensity > 0.7 ? 'high' : intensity > 0.4 ? 'medium' : intensity > 0.1 ? 'low' : 'none';
511 |             html += `<div class="heatmap-cell ${intensityClass}" title="${day} ${hour}:00 - Activity: ${Math.round(intensity * 100)}%"></div>`;
512 |           });
513 |           html += `</div>`;
514 |         });
515 | 
516 |         html += '</div>';
517 |         return html;
518 |       }
519 | 
520 |       // Event Listeners
521 |       function initializeEventListeners() {
522 |         // Sort buttons
523 |         document.querySelectorAll('.sort-btn').forEach(btn => {
524 |           btn.addEventListener('click', (e) => {
525 |             document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
526 |             e.target.classList.add('active');
527 |             sortAgents(e.target.dataset.sort);
528 |           });
529 |         });
530 | 
531 |         // Time range selector
532 |         document.querySelectorAll('.time-btn').forEach(btn => {
533 |           btn.addEventListener('click', (e) => {
534 |             document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
535 |             e.target.classList.add('active');
536 |             updateChartsForTimeRange(e.target.dataset.range);
537 |           });
538 |         });
539 | 
540 |         // Chart metric toggles
541 |         document.querySelectorAll('.chart-toggle').forEach(btn => {
542 |           btn.addEventListener('click', (e) => {
543 |             document.querySelectorAll('.chart-toggle').forEach(b => b.classList.remove('active'));
544 |             e.target.classList.add('active');
545 |             updatePerformanceChart(e.target.dataset.metric);
546 |           });
547 |         });
548 | 
549 |         // Agent comparison selectors
550 |         document.getElementById('agent1Select').addEventListener('change', updateComparisonChart);
551 |         document.getElementById('agent2Select').addEventListener('change', updateComparisonChart);
552 | 
553 |         // Activity feed filters
554 |         document.querySelectorAll('.filter-btn').forEach(btn => {
555 |           btn.addEventListener('click', (e) => {
556 |             document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
557 |             e.target.classList.add('active');
558 |             filterActivityFeed(e.target.dataset.filter);
559 |           });
560 |         });
561 |       }
562 | 
563 |       // Utility Functions
564 |       function sortAgents(criteria) {
565 |         // Implementation for sorting agent cards
566 |         console.log(`Sorting by: ${criteria}`);
567 |       }
568 | 
569 |       function updateChartsForTimeRange(range) {
570 |         // Implementation for updating charts based on time range
571 |         console.log(`Updating charts for: ${range}`);
572 |       }
573 | 
574 |       function updatePerformanceChart(metric) {
575 |         // Implementation for switching performance chart metrics
576 |         console.log(`Switching to metric: ${metric}`);
577 |       }
578 | 
579 |       function updateComparisonChart() {
580 |         const agent1 = document.getElementById('agent1Select').value;
581 |         const agent2 = document.getElementById('agent2Select').value;
582 |         console.log(`Comparing ${agent1} vs ${agent2}`);
583 |       }
584 | 
585 |       function filterActivityFeed(filter) {
586 |         // Implementation for filtering activity feed
587 |         console.log(`Filtering by: ${filter}`);
588 |       }
589 | 
590 |       function showAgentDetails(agentKey) {
591 |         // Show modal with detailed agent information
592 |         const modal = document.getElementById('agentModal');
593 |         modal.style.display = 'block';
594 | 
595 |         // Populate modal content
596 |         document.getElementById('modalAgentName').textContent = `${agentKey} Details`;
597 |         document.getElementById('modalBody').innerHTML = generateAgentDetailsHTML(agentKey);
598 |       }
599 | 
600 |       function closeAgentModal() {
601 |         document.getElementById('agentModal').style.display = 'none';
602 |       }
603 | 
604 |       function generateAgentDetailsHTML(agentKey) {
605 |         return `
606 |           <div class="agent-details">
607 |             <p>Detailed analytics for ${agentKey} will be displayed here.</p>
608 |             <div class="detail-charts">
609 |               <!-- Additional charts and metrics -->
610 |             </div>
611 |           </div>
612 |         `;
613 |       }
614 | 
615 |       function startLiveUpdates() {
616 |         // Simulate live updates every 30 seconds
617 |         setInterval(() => {
618 |           updateLiveIndicator();
619 |           // Add new activity items
620 |           addActivityItem();
621 |         }, 30000);
622 |       }
623 | 
624 |       function updateLiveIndicator() {
625 |         const indicator = document.querySelector('.pulse-dot');
626 |         indicator.style.animation = 'none';
627 |         setTimeout(() => {
628 |           indicator.style.animation = 'pulse 2s infinite';
629 |         }, 10);
630 |       }
631 | 
632 |       function addActivityItem() {
633 |         const feed = document.getElementById('activityFeed');
634 |         const agents = ['GitHub Copilot', 'OpenAI Codex', 'Cursor Agents', 'Devin', 'Codegen'];
635 |         const randomAgent = agents[Math.floor(Math.random() * agents.length)];
636 |         const actions = ['created', 'merged', 'updated'];
637 |         const randomAction = actions[Math.floor(Math.random() * actions.length)];
638 | 
639 |         const activityItem = document.createElement('div');
640 |         activityItem.className = 'activity-item';
641 |         activityItem.innerHTML = `
642 |           <div class="activity-icon ${randomAction}">
643 |             <i class="fas fa-${randomAction === 'created' ? 'plus' : randomAction === 'merged' ? 'check' : 'edit'}"></i>
644 |           </div>
645 |           <div class="activity-content">
646 |             <div class="activity-text">
647 |               <strong>${randomAgent}</strong> ${randomAction} a pull request
648 |             </div>
649 |             <div class="activity-time">Just now</div>
650 |           </div>
651 |         `;
652 | 
653 |         feed.insertBefore(activityItem, feed.firstChild);
654 | 
655 |         // Keep only last 10 items
656 |         while (feed.children.length > 10) {
657 |           feed.removeChild(feed.lastChild);
658 |         }
659 |       }
660 | 
661 |       // Initialize activity feed with some items
662 |       document.addEventListener('DOMContentLoaded', function() {
663 |         for (let i = 0; i < 5; i++) {
664 |           setTimeout(() => addActivityItem(), i * 1000);
665 |         }
666 |       });
667 | 
668 |       // Modal click outside to close
669 |       window.onclick = function(event) {
670 |         const modal = document.getElementById('agentModal');
671 |         if (event.target === modal) {
672 |           modal.style.display = 'none';
673 |         }
674 |       }
675 |     </script>
676 |   </body>
677 | </html>
678 |               mode: "point",
679 |             },
680 |             plugins: {
681 |               title: {
682 |                 display: true,
683 |                 text: "PR Volume vs Success Rate",
684 |                 font: {
685 |                   size: 16,
686 |                   weight: "bold",
687 |                 },
688 |               },
689 |               legend: {
690 |                 display: true,
691 |                 position: "right",
692 |                 labels: {
693 |                   usePointStyle: true,
694 |                   pointStyle: "circle",
695 |                   font: {
696 |                     size: 11,
697 |                   },
698 |                 },
699 |               },
700 |               tooltip: {
701 |                 callbacks: {
702 |                   title: function (context) {
703 |                     return context[0].label;
704 |                   },
705 |                   label: function (context) {
706 |                     const datasetLabel = context.dataset.label;
707 |                     const value = context.parsed.y;
708 | 
709 |                     if (datasetLabel.includes("Success %")) {
710 |                       return datasetLabel + ": " + value.toFixed(1) + "%";
711 |                     } else {
712 |                       return (
713 |                         datasetLabel + ": " + value.toLocaleString() + " PRs"
714 |                       );
715 |                     }
716 |                   },
717 |                 },
718 |               },
719 |             },
720 |             scales: {
721 |               x: {
722 |                 title: {
723 |                   display: true,
724 |                   text: "Time",
725 |                 },
726 |               },
727 |               y: {
728 |                 type: "linear",
729 |                 display: true,
730 |                 position: "left",
731 |                 title: {
732 |                   display: true,
733 |                   text: "Number of PRs",
734 |                 },
735 |                 beginAtZero: true,
736 |               },
737 |               y1: {
738 |                 type: "linear",
739 |                 display: true,
740 |                 position: "right",
741 |                 title: {
742 |                   display: true,
743 |                   text: "Success Rate (%)",
744 |                 },
745 |                 beginAtZero: true,
746 |                 max: 100,
747 |                 grid: {
748 |                   drawOnChartArea: false,
749 |                 },
750 |               },
751 |             },
752 |           },
753 |         });
754 | 
755 |         // Hide fallback and show interactive chart
756 |         document.querySelector(".chart-fallback").style.display = "none";
757 |         document.querySelector(".chart-container").style.display = "block";
758 |         document.querySelector(".chart-controls").style.display = "block";
759 | 
760 |         // Update statistics table with latest data
761 |         updateStatisticsTable();
762 | 
763 |         // Set up toggle functionality
764 |         setupToggleButtons();
765 |       }
766 | 
767 |       function updateStatisticsTable() {
768 |         if (!chartData || !chartData.datasets) return;
769 | 
770 |         // Get the latest data point (last index)
771 |         const latestIndex = chartData.labels.length - 1;
772 | 
773 |         // Dynamically generated from server-side AGENTS list
774 |         const agents = {{ agents | tojson | safe }};
775 | 
776 |         agents.forEach((agent) => {
777 |           // Find datasets for this agent
778 |           const totalDataset = chartData.datasets.find(
779 |             (d) =>
780 |               d.label.toLowerCase().includes(agent.key) &&
781 |               d.label.toLowerCase().includes("total")
782 |           );
783 |           const mergedDataset = chartData.datasets.find(
784 |             (d) =>
785 |               d.label.toLowerCase().includes(agent.key) &&
786 |               d.label.toLowerCase().includes("merged")
787 |           );
788 | 
789 |           if (totalDataset && mergedDataset) {
790 |             const total = totalDataset.data[latestIndex] || 0;
791 |             const merged = mergedDataset.data[latestIndex] || 0;
792 |             const rate = total > 0 ? (merged / total) * 100 : 0;
793 | 
794 |             // Update table cells
795 |             const totalElement = document.getElementById(`${agent}-total`);
796 |             const mergedElement = document.getElementById(`${agent}-merged`);
797 |             const rateElement = document.getElementById(`${agent}-rate`);
798 | 
799 |             if (totalElement) totalElement.textContent = total.toLocaleString();
800 |             if (mergedElement)
801 |               mergedElement.textContent = merged.toLocaleString();
802 |             if (rateElement) rateElement.textContent = rate.toFixed(1) + "%";
803 |           }
804 |         });
805 |       }
806 | 
807 |       function setupToggleButtons() {
808 |         const toggleButtons = document.querySelectorAll(".toggle-btn");
809 |         const viewButtons = document.querySelectorAll(".view-btn");
810 | 
811 |         // Agent toggle functionality
812 |         toggleButtons.forEach((button) => {
813 |           button.addEventListener("click", function () {
814 |             const agent = this.dataset.agent;
815 |             const isActive = this.classList.contains("active");
816 | 
817 |             // Toggle button state
818 |             this.classList.toggle("active");
819 | 
820 |             // Get current view mode
821 |             const activeViewBtn = document.querySelector(".view-btn.active");
822 |             const currentView = activeViewBtn
823 |               ? activeViewBtn.dataset.view
824 |               : "all";
825 | 
826 |             // Find and toggle datasets for this agent
827 |             chartInstance.data.datasets.forEach((dataset, index) => {
828 |               const labelLower = dataset.label.toLowerCase();
829 |               if (labelLower.includes(agent)) {
830 |                 const isLine = dataset.type === "line";
831 |                 const isBar = dataset.type === "bar";
832 | 
833 |                 if (isActive) {
834 |                   // Was active, now hiding
835 |                   dataset.hidden = true;
836 |                 } else {
837 |                   // Was hidden, now showing (but respect view mode)
838 |                   switch (currentView) {
839 |                     case "all":
840 |                       dataset.hidden = false;
841 |                       break;
842 |                     case "bars":
843 |                       dataset.hidden = isLine;
844 |                       break;
845 |                     case "lines":
846 |                       dataset.hidden = isBar;
847 |                       break;
848 |                   }
849 |                 }
850 |               }
851 |             });
852 | 
853 |             chartInstance.update();
854 |           });
855 |         });
856 | 
857 |         // View mode functionality
858 |         viewButtons.forEach((button) => {
859 |           button.addEventListener("click", function () {
860 |             const view = this.dataset.view;
861 | 
862 |             // Update button states
863 |             viewButtons.forEach((btn) => btn.classList.remove("active"));
864 |             this.classList.add("active");
865 | 
866 |             // Show/hide datasets based on view mode and agent toggles
867 |             chartInstance.data.datasets.forEach((dataset) => {
868 |               const isLine = dataset.type === "line";
869 |               const isBar = dataset.type === "bar";
870 | 
871 |               // Get the agent name from the dataset label
872 |               const agentMatch = dataset.label
873 |                 .toLowerCase()
874 |                 .match(/(copilot|codex|cursor|devin|codegen)/);
875 |               const agent = agentMatch ? agentMatch[1] : null;
876 | 
877 |               // Check if agent is currently enabled
878 |               const agentButton = document.querySelector(
879 |                 `.toggle-btn[data-agent="${agent}"]`
880 |               );
881 |               const agentEnabled =
882 |                 agentButton && agentButton.classList.contains("active");
883 | 
884 |               switch (view) {
885 |                 case "all":
886 |                   dataset.hidden = !agentEnabled;
887 |                   break;
888 |                 case "bars":
889 |                   dataset.hidden = !agentEnabled || isLine;
890 |                   break;
891 |                 case "lines":
892 |                   dataset.hidden = !agentEnabled || isBar;
893 |                   break;
894 |               }
895 |             });
896 | 
897 |             chartInstance.update();
898 |           });
899 |         });
900 |       }
901 | 
902 |       // Initialize when page loads
903 |       document.addEventListener("DOMContentLoaded", function () {
904 |         loadChartData();
905 | 
906 |         // Update last updated timestamp
907 |         const now = new Date();
908 |         const formattedDate = now.toLocaleString("en-US", {
909 |           year: "numeric",
910 |           month: "long",
911 |           day: "2-digit",
912 |           hour: "2-digit",
913 |           minute: "2-digit",
914 |           timeZone: "UTC",
915 |           timeZoneName: "short",
916 |         });
917 |         document.getElementById("last-updated").textContent = formattedDate;
918 |       });
919 |     </script>
920 |   </body>
921 | </html>
922 | 


--------------------------------------------------------------------------------
/templates/index_template_simple.html:
--------------------------------------------------------------------------------
  1 | <!DOCTYPE html>
  2 | <html lang="en">
  3 |   <head>
  4 |     <meta charset="UTF-8" />
  5 |     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6 |     <title>AI Coding Agents</title>
  7 |     <link rel="stylesheet" href="styles.css" />
  8 |     <meta http-equiv="refresh" content="3600" />
  9 |     <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
 10 |     <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
 11 |     <link rel="preconnect" href="https://fonts.googleapis.com" />
 12 |     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
 13 |     <link
 14 |       href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
 15 |       rel="stylesheet"
 16 |     />
 17 |   </head>
 18 |   <body>
 19 |     <div class="container">
 20 |       <header class="hero">
 21 |         <h1>AI Coding Agents</h1>
 22 |         <p>Which AI writes the most successful code?</p>
 23 |       </header>
 24 | 
 25 |       <!-- Simple Leaderboard -->
 26 |       <section class="leaderboard">
 27 |         {% for agent in agents %}
 28 |         <div class="agent" data-success-rate="{{ stats[agent.key].rate }}">
 29 |           <div class="agent-header">
 30 |             <div class="rank">#{{ loop.index }}</div>
 31 |             <div
 32 |               class="agent-dot"
 33 |               style="background-color: {{ agent.color }}"
 34 |             ></div>
 35 |             <div class="agent-info">
 36 |               <h3>
 37 |                 <a href="{{ agent.info_url }}" target="_blank"
 38 |                   >{{ agent.long_name }}</a
 39 |                 >
 40 |               </h3>
 41 |               <span class="agent-subtitle"
 42 |                 >{{ stats[agent.key].total | comma }} PRs</span
 43 |               >
 44 |             </div>
 45 |           </div>
 46 |           <div class="metrics">
 47 |             <div class="primary-metric">
 48 |               <span class="metric-value"
 49 |                 >{{ stats[agent.key].rate | round(1) }}%</span
 50 |               >
 51 |               <span class="metric-label">Success Rate</span>
 52 |             </div>
 53 |             <div class="secondary-metrics">
 54 |               <div class="metric">
 55 |                 <a href="{{ agent.merged_query_url }}" target="_blank"
 56 |                   >{{ stats[agent.key].merged | comma }}</a
 57 |                 >
 58 |                 <span>merged</span>
 59 |               </div>
 60 |             </div>
 61 |           </div>
 62 |           <div class="progress-bar">
 63 |             <div
 64 |               class="progress"
 65 |               style="width: {{ stats[agent.key].rate }}%; background-color: {{ agent.color }}"
 66 |             ></div>
 67 |           </div>
 68 |         </div>
 69 |         {% endfor %}
 70 |       </section>
 71 | 
 72 |       <!-- Simple Chart -->
 73 |       <section class="chart-section">
 74 |         <h2>Performance Over Time</h2>
 75 |         <div class="chart-container">
 76 |           <canvas id="mainChart"></canvas>
 77 |         </div>
 78 |       </section>
 79 | 
 80 |       <!-- Simple Footer -->
 81 |       <footer class="footer">
 82 |         <p>
 83 |           Updated {{ timestamp }} •
 84 |           <a href="https://github.com/aavetis/ai-pr-watcher">GitHub</a>
 85 |         </p>
 86 |       </footer>
 87 |     </div>
 88 | 
 89 |     <script>
 90 |       // Simple chart initialization
 91 |       let chart = null;
 92 | 
 93 |       document.addEventListener("DOMContentLoaded", function () {
 94 |         loadChart();
 95 |       });
 96 | 
 97 |       async function loadChart() {
 98 |         try {
 99 |           const response = await fetch("chart-data.json");
100 |           const data = await response.json();
101 |           initChart(data);
102 |         } catch (error) {
103 |           console.error("Chart failed to load:", error);
104 |         }
105 |       }
106 | 
107 |       function initChart(data) {
108 |         const ctx = document.getElementById("mainChart").getContext("2d");
109 | 
110 |         chart = new Chart(ctx, {
111 |           type: "line",
112 |           data: data,
113 |           options: {
114 |             responsive: true,
115 |             maintainAspectRatio: false,
116 |             plugins: {
117 |               legend: {
118 |                 position: "top",
119 |                 labels: {
120 |                   color: "#94a3b8",
121 |                   font: { family: "Inter", size: 12 },
122 |                   padding: 20,
123 |                   usePointStyle: true,
124 |                 },
125 |               },
126 |             },
127 |             scales: {
128 |               x: {
129 |                 type: "time",
130 |                 time: { unit: "day" },
131 |                 grid: { color: "#334155" },
132 |                 ticks: { color: "#94a3b8" },
133 |               },
134 |               y: {
135 |                 grid: { color: "#334155" },
136 |                 ticks: { color: "#94a3b8" },
137 |               },
138 |             },
139 |           },
140 |         });
141 |       }
142 |     </script>
143 |   </body>
144 | </html>
145 | 


--------------------------------------------------------------------------------
/templates/readme_template.md:
--------------------------------------------------------------------------------
 1 | ### PR Analytics: Volume vs Success Rate (auto-updated)
 2 | 
 3 | View the [interactive dashboard](https://prarena.ai) for these statistics.
 4 | 
 5 | ## Understanding the Metrics
 6 | 
 7 | Different AI coding agents follow different workflows when creating pull requests:
 8 | 
 9 | - **All PRs**: Every pull request created by an agent, including DRAFT PRs
10 | - **Ready PRs**: Non-draft pull requests that are ready for review and merging
11 | - **Merged PRs**: Pull requests that were successfully merged into the codebase
12 | 
13 | **Key workflow differences**: Some agents like **Codex** iterate privately and create ready PRs directly, resulting in very few drafts but high merge rates. Others like **Copilot** and **Codegen** create draft PRs first, encouraging public iteration before marking them ready for review.
14 | 
15 | The statistics below focus on **Ready PRs only** to fairly compare agents across different workflows, measuring each agent's ability to produce mergeable code regardless of whether they iterate publicly (with drafts) or privately.
16 | 
17 | ## Data sources
18 | 
19 | Explore the GitHub search queries used:
20 | 
21 | {% for agent in agents %}
22 | 
23 | - **All {{ agent.display }} PRs**: [{{ agent.total_query_url }}]({{ agent.total_query_url }})
24 | - **Merged {{ agent.display }} PRs**: [{{ agent.merged_query_url }}]({{ agent.merged_query_url }})
25 |   {% endfor %}
26 | 
27 | ---
28 | 
29 | ![chart](docs/chart.png)
30 | 
31 | ## Current Statistics
32 | 
33 | | Project | Ready PRs | Merged PRs | Success Rate |
34 | | ------- | --------- | ---------- | ------------ |{%- for agent in agents %}
35 | | {{ agent.display }} | {{ stats[agent.key].nondraft | comma }} | {{ stats[agent.key].merged | comma }} | {{ stats[agent.key].rate | round(2) }}% |{%- endfor %}
36 | 


--------------------------------------------------------------------------------