├── .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 |  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()">×</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 |  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 | --------------------------------------------------------------------------------