Skip to content

Commit 18146e8

Browse files
authored
feat: add AI coding stats from WakaTime (badge, weekly breakdown, insights) (#671)
WakaTime's existing stats endpoints already expose AI-coding data (lines by AI vs. human, cost, tokens, sessions/prompts, per-model breakdown), so no new API integration was needed. - Add SHOW_AI_CODE_TIME (all-time AI Code Time badge, sourced from stats/all_time since all_time_since_today lacks AI fields) and SHOW_AI_CODING (weekly breakdown: lines, tokens, cost, sessions, per-model split) flags, both default True. - Deduce a few insights purely from the raw ratios (AI reliance, prompt style, session style, review style) rather than just dumping numbers, mirroring the existing "I'm an Early"/"Most Productive on" style. - Render a "No AI Coding Activity" fallback instead of hiding the section when an account has no AI data that week. - Update mocks to match the real schema and propagate new translation keys across all 22 locales.
1 parent 6a2ec22 commit 18146e8

9 files changed

Lines changed: 791 additions & 30 deletions

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ INPUT_SHOW_LINES_OF_CODE=True
1414
INPUT_SHOW_LOC_CHART=True
1515
INPUT_SHOW_PROFILE_VIEWS=True
1616
INPUT_SHOW_TOTAL_CODE_TIME=True
17+
INPUT_SHOW_AI_CODE_TIME=True
18+
INPUT_SHOW_AI_CODING=True
1719
INPUT_SHOW_SHORT_INFO=True
1820
INPUT_SHOW_COMMIT=True
1921
INPUT_SHOW_DAYS_OF_WEEK=True

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,50 @@ The `SHOW_TOTAL_CODE_TIME` flag can be set to `False` to hide *Code Time*.
157157

158158
![Code Time](http://img.shields.io/badge/Code%20Time-1%2C438%20hrs%2054%20mins-blue)
159159

160+
> [!NOTE]
161+
> The `SHOW_AI_CODE_TIME` and `SHOW_AI_CODING` flags below require WakaTime's AI coding tracking to be recording activity on your account. If your account has no all-time AI data the **AI Code Time** badge is hidden entirely; if it has no AI data for the current week, the weekly block still shows with a "No AI Coding Activity Tracked This Week" message instead of numbers.
162+
163+
The `SHOW_AI_CODE_TIME` flag can be set to `False` to hide the all-time **AI Code Time** badge.
164+
165+
![AI Code Time](http://img.shields.io/badge/AI%20Code%20Time-77%20hrs%2022%20mins-blue)
166+
167+
The `SHOW_AI_CODING` flag can be set to `False` to hide the weekly AI coding breakdown: AI coding time, AI vs. human written lines, token usage, estimated AI cost, sessions/prompts, a per-model breakdown, and a few deduced insights.
168+
169+
**🤖 AI Coding This Week**
170+
171+
```text
172+
⏱ AI Coding Time: 1 hr 53 mins (3.59%)
173+
174+
✍️ 1,245 lines written by AI, 3,120 lines written by hand (28.52% AI-written)
175+
176+
🔤 845,000 Input Tokens, 21,000 Output Tokens
177+
178+
💵 $12.48 Estimated AI Cost This Week
179+
180+
🧠 5 AI Sessions, 20 AI Prompts
181+
182+
Sonnet 1,200 lines ██████████████████████░░░ 89.96 %
183+
GPT-4 134 lines ███░░░░░░░░░░░░░░░░░░░░░░ 10.04 %
184+
185+
🔎 AI Coding Insights:
186+
🧑‍💻 Mostly Hands-On — 28.52% of written lines came from AI
187+
📄 Detailed Prompter — average 925 characters per prompt
188+
🔁 Iterative Prompter — average 4 prompts per session
189+
🔍 Hands-On Reviewer — 73.29% of changed lines were hand-edited
190+
```
191+
192+
The insight lines are all deduced from the raw numbers above, not extra API data:
193+
- **AI Reliance** (`🤖 AI-Driven` / `⚖️ Balanced with AI` / `🧑‍💻 Mostly Hands-On`) — from the share of added lines written by AI.
194+
- **Prompt Style** (`📝 Concise` / `📄 Detailed` / `📚 Verbose`) — from the average prompt length.
195+
- **Session Style** (`🎯 One-Shot` / `🔁 Iterative`) — from the average number of prompts per AI session, i.e. whether you tend to get it right in one prompt or rely on follow-ups.
196+
- **Review Style** (`🔍 Hands-On Reviewer` / `🚀 High AI Trust`) — from the share of all changed lines that were still hand-edited, as a proxy for how much AI output gets manually reviewed/reworked.
197+
198+
If there was no AI coding activity that week, the block still renders with a fallback message instead of disappearing:
199+
200+
```text
201+
No AI Coding Activity Tracked This Week
202+
```
203+
160204
The `SHOW_PROFILE_VIEWS` flag can be set to `False` to hide **Profile Views**
161205

162206
![Profile Views](http://img.shields.io/badge/Profile%20Views-2189-blue)

action.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,16 @@ inputs:
102102
description: "Show Total Time you have coded"
103103
default: "True"
104104

105+
SHOW_AI_CODE_TIME:
106+
required: false
107+
description: "Show the all-time AI Code Time badge"
108+
default: "True"
109+
110+
SHOW_AI_CODING:
111+
required: false
112+
description: "Show the weekly AI coding breakdown (AI vs human lines, cost, model breakdown, sessions)"
113+
default: "True"
114+
105115
COMMIT_BY_ME:
106116
required: false
107117
description: "Git commit with your own name and email"

sources/main.py

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from asyncio import run
66
from datetime import datetime
7-
from typing import Dict
7+
from typing import Dict, List, Optional
88
from urllib.parse import quote
99

1010
from humanize import intword, naturalsize, intcomma
@@ -23,6 +23,106 @@
2323
)
2424

2525

26+
def find_category(categories: List[Dict], name: str) -> Optional[Dict]:
27+
"""
28+
Find a WakaTime category entry (e.g. "AI Coding") by name in a stats response's `categories` list.
29+
30+
:param categories: List of category dictionaries from a WakaTime stats response.
31+
:param name: Category name to look for.
32+
:returns: The matching category dictionary, or None if not found.
33+
"""
34+
return next((category for category in categories if category["name"] == name), None)
35+
36+
37+
def make_ai_coding_insights(ai_written_percent: float, prompt_length_avg: float, prompts_per_session: float, manual_touch_percent: float) -> str:
38+
"""
39+
Deduce a few human-readable insight lines from the raw AI coding numbers:
40+
how AI-reliant the week was, prompting style (length), session style (one-shot vs. follow-ups),
41+
and how much of the changed code was still touched by hand (a proxy for manual review).
42+
All tiers are computed purely from ratios already present in the WakaTime response, no extra API calls.
43+
44+
:param ai_written_percent: Share of added lines written by AI (0-100).
45+
:param prompt_length_avg: Average prompt length in characters.
46+
:param prompts_per_session: Average number of prompts per AI session.
47+
:param manual_touch_percent: Share of all changed lines (additions + deletions) that were human-made (0-100).
48+
:returns: String representation of the insight lines.
49+
"""
50+
# Thresholds are heuristic tiers over continuous ratios, not WakaTime-defined categories.
51+
reliance_label = (
52+
FM.t("AI Reliance: AI-Driven")
53+
if ai_written_percent >= 66
54+
else FM.t("AI Reliance: Balanced") if ai_written_percent >= 33 else FM.t("AI Reliance: Hands-On")
55+
)
56+
prompt_style_label = (
57+
FM.t("Prompt Style: Verbose")
58+
if prompt_length_avg > 1500
59+
else FM.t("Prompt Style: Detailed") if prompt_length_avg >= 500 else FM.t("Prompt Style: Concise")
60+
)
61+
session_style_label = FM.t("Session Style: Iterative") if prompts_per_session > 1.5 else FM.t("Session Style: One-Shot")
62+
review_label = FM.t("Review Style: Hands-On Reviewer") if manual_touch_percent >= 50 else FM.t("Review Style: High AI Trust")
63+
64+
insights = f"🔎 {FM.t('AI Coding Insights')}:\n"
65+
insights += f"{FM.t('AI Reliance Detail') % (reliance_label, round(ai_written_percent, 2))}\n"
66+
insights += f"{FM.t('Prompt Style Detail') % (prompt_style_label, intcomma(round(prompt_length_avg)))}\n"
67+
insights += f"{FM.t('Session Style Detail') % (session_style_label, round(prompts_per_session, 1))}\n"
68+
insights += f"{FM.t('Review Style Detail') % (review_label, round(manual_touch_percent, 2))}\n"
69+
return insights
70+
71+
72+
def make_ai_coding_stats(data: Dict) -> str:
73+
"""
74+
Build the weekly AI coding stats block: AI coding time, AI vs human written lines,
75+
token usage, estimated AI cost, sessions/prompts, per-model breakdown and deduced insights.
76+
Renders a "no activity" fallback (instead of hiding the section) if the account has no AI coding data this week.
77+
78+
:param data: WakaTime weekly stats response (`waka_latest`).
79+
:returns: String representation of the AI coding stats.
80+
"""
81+
ai_category = find_category(data["data"].get("categories", []), "AI Coding")
82+
ai_sessions = data["data"].get("ai_sessions", 0)
83+
84+
stats = f"🤖 **{FM.t('AI Coding This Week')}** \n\n```text\n"
85+
86+
if ai_category is None or not ai_sessions:
87+
stats += f"{FM.t('No AI Coding Activity Tracked This Week')}\n\n"
88+
return f"{stats[:-1]}```\n\n"
89+
90+
ai_additions = data["data"].get("ai_additions", 0)
91+
ai_deletions = data["data"].get("ai_deletions", 0)
92+
human_additions = data["data"].get("human_additions", 0)
93+
human_deletions = data["data"].get("human_deletions", 0)
94+
ai_input_tokens = data["data"].get("ai_input_tokens", 0)
95+
ai_output_tokens = data["data"].get("ai_output_tokens", 0)
96+
ai_cost = data["data"].get("ai_model_total_cost", 0)
97+
ai_prompts = data["data"].get("ai_prompt_events_total", 0)
98+
prompt_length_avg = data["data"].get("ai_prompt_length_avg", 0)
99+
prompts_per_session = data["data"].get("ai_prompt_events_avg_per_session", 0)
100+
101+
total_additions = ai_additions + human_additions
102+
ai_written_percent = (ai_additions / total_additions * 100) if total_additions else 0
103+
104+
total_changes = ai_additions + ai_deletions + human_additions + human_deletions
105+
manual_touch_percent = ((human_additions + human_deletions) / total_changes * 100) if total_changes else 0
106+
107+
stats += f"⏱ {FM.t('AI Coding Time')}: {ai_category['text']} ({ai_category['percent']}%)\n\n"
108+
stats += f"✍️ {FM.t('AI vs Human Lines') % (intcomma(ai_additions), intcomma(human_additions), round(ai_written_percent, 2))}\n\n"
109+
stats += f"🔤 {FM.t('AI Token Usage') % (intcomma(ai_input_tokens), intcomma(ai_output_tokens))}\n\n"
110+
stats += f"💵 {FM.t('Estimated AI Cost') % f'{ai_cost:.2f}'}\n\n"
111+
stats += f"🧠 {FM.t('AI Sessions and Prompts') % (ai_sessions, ai_prompts)}\n\n"
112+
113+
ai_model_breakdown = data["data"].get("ai_model_breakdown", [])
114+
if ai_model_breakdown:
115+
total_lines = sum(model["lines"] for model in ai_model_breakdown) or 1
116+
names = [model["name"] for model in ai_model_breakdown]
117+
texts = [f"{intcomma(model['lines'])} lines" for model in ai_model_breakdown]
118+
percents = [round(model["lines"] / total_lines * 100, 2) for model in ai_model_breakdown]
119+
stats += f"{make_list(names=names, texts=texts, percents=percents)}\n\n"
120+
121+
stats += f"{make_ai_coding_insights(ai_written_percent, prompt_length_avg, prompts_per_session, manual_touch_percent)}\n"
122+
123+
return f"{stats[:-1]}```\n\n"
124+
125+
26126
async def get_waka_time_stats(repositories: Dict, commit_dates: Dict) -> str:
27127
"""
28128
Collects user info from wakatime.
@@ -74,6 +174,10 @@ async def get_waka_time_stats(repositories: Dict, commit_dates: Dict) -> str:
74174

75175
stats = f"{stats[:-1]}```\n\n"
76176

177+
if EM.SHOW_AI_CODING:
178+
DBM.i("Adding AI coding stats...")
179+
stats += make_ai_coding_stats(data)
180+
77181
DBM.g("WakaTime stats added!")
78182
return stats
79183

@@ -193,13 +297,28 @@ async def get_stats() -> str:
193297
yearly_data, commit_data = dict(), dict()
194298
DBM.w("User yearly data not needed, skipped.")
195299

196-
if EM.SHOW_TOTAL_CODE_TIME:
300+
if EM.SHOW_TOTAL_CODE_TIME or EM.SHOW_AI_CODE_TIME:
197301
DBM.i("Adding total code time info...")
198302
data = await DM.get_remote_json("waka_all")
199303
if data is None:
200304
DBM.p("WakaTime data unavailable!")
201305
else:
202-
stats += f"![Code Time](http://img.shields.io/badge/{quote('Code Time')}-{quote(str(data['data']['text']))}-blue?style={quote(EM.BADGE_STYLE)})\n\n"
306+
if EM.SHOW_TOTAL_CODE_TIME:
307+
stats += (
308+
f"![Code Time](http://img.shields.io/badge/{quote('Code Time')}-"
309+
f"{quote(str(data['data']['human_readable_total']))}-blue?style={quote(EM.BADGE_STYLE)})\n\n"
310+
)
311+
312+
if EM.SHOW_AI_CODE_TIME:
313+
DBM.i("Adding AI code time info...")
314+
ai_category = find_category(data["data"].get("categories", []), "AI Coding")
315+
if ai_category is None:
316+
DBM.w("No all-time AI coding data available, skipping AI Code Time badge.")
317+
else:
318+
stats += (
319+
f"![AI Code Time](http://img.shields.io/badge/{quote('AI Code Time')}-"
320+
f"{quote(str(ai_category['text']))}-blue?style={quote(EM.BADGE_STYLE)})\n\n"
321+
)
203322

204323
if EM.SHOW_PROFILE_VIEWS:
205324
if EM.DEBUG_RUN or GHM.REMOTE is None:

sources/manager_download.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ async def init_download_manager(user_login: str):
130130
await DownloadManager.load_remote_resources(
131131
linguist="https://cdn.jsdelivr.net/gh/github/linguist@master/lib/linguist/languages.yml",
132132
waka_latest=f"{EM.WAKATIME_API_URL}users/current/stats/last_7_days?api_key={EM.WAKATIME_API_KEY}",
133-
waka_all=f"{EM.WAKATIME_API_URL}users/current/all_time_since_today?api_key={EM.WAKATIME_API_KEY}",
133+
waka_all=f"{EM.WAKATIME_API_URL}users/current/stats/all_time?api_key={EM.WAKATIME_API_KEY}",
134134
github_stats=f"https://github-contributions.vercel.app/api/v1/{user_login}",
135135
)
136136

sources/manager_environment.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ class EnvironmentManager:
4949
SHOW_SHORT_INFO = getenv("INPUT_SHOW_SHORT_INFO", "True").lower() in _TRUTHY
5050
SHOW_UPDATED_DATE = getenv("INPUT_SHOW_UPDATED_DATE", "True").lower() in _TRUTHY
5151
SHOW_TOTAL_CODE_TIME = getenv("INPUT_SHOW_TOTAL_CODE_TIME", "True").lower() in _TRUTHY
52+
SHOW_AI_CODE_TIME = getenv("INPUT_SHOW_AI_CODE_TIME", "True").lower() in _TRUTHY
53+
SHOW_AI_CODING = getenv("INPUT_SHOW_AI_CODING", "True").lower() in _TRUTHY
5254

5355
COMMIT_BY_ME = getenv("INPUT_COMMIT_BY_ME", "False").lower() in _TRUTHY
5456
COMMIT_MESSAGE = getenv("INPUT_COMMIT_MESSAGE", "Updated with Dev Metrics")
Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,47 @@
11
{
2-
"data": {
3-
"text": "test"
4-
}
2+
"data": {
3+
"id": "00000000-0000-0000-0000-000000000000",
4+
"user_id": "11111111-1111-1111-1111-111111111111",
5+
"range": "all_time",
6+
"start": "2020-07-14T00:00:00Z",
7+
"end": "2026-07-27T23:59:59Z",
8+
"timezone": "UTC",
9+
"status": "ok",
10+
"total_seconds": 19356005.587,
11+
"human_readable_total": "5,376 hrs 40 mins",
12+
"is_up_to_date": true,
13+
"percent_calculated": 100,
14+
"categories": [
15+
{
16+
"name": "Coding",
17+
"total_seconds": 19021442.488,
18+
"percent": 98.27,
19+
"digital": "5283:44",
20+
"decimal": "5283.73",
21+
"text": "5,283 hrs 44 mins",
22+
"hours": 5283,
23+
"minutes": 44
24+
},
25+
{
26+
"name": "AI Coding",
27+
"total_seconds": 278541.827,
28+
"percent": 1.44,
29+
"digital": "77:22",
30+
"decimal": "77.37",
31+
"text": "77 hrs 22 mins",
32+
"hours": 77,
33+
"minutes": 22
34+
},
35+
{
36+
"name": "Writing Docs",
37+
"total_seconds": 49815.63,
38+
"percent": 0.26,
39+
"digital": "13:50",
40+
"decimal": "13.83",
41+
"text": "13 hrs 50 mins",
42+
"hours": 13,
43+
"minutes": 50
44+
}
45+
]
46+
}
547
}

sources/mock_data/mock_wakatime_stats.json

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,42 @@
88
"timeout": 15,
99
"writes_only": false,
1010
"timezone": "UTC",
11+
"ai_additions": 1245,
12+
"ai_deletions": 89,
13+
"human_additions": 3120,
14+
"human_deletions": 540,
15+
"ai_line_changes_total": 1334,
16+
"ai_model_total_cost": 12.4821,
17+
"ai_input_tokens": 845000,
18+
"ai_output_tokens": 21000,
19+
"ai_prompt_length_sum": 18500,
20+
"ai_prompt_length_avg": 925,
21+
"ai_prompt_events_total": 20,
22+
"ai_prompt_events_avg_per_session": 4,
23+
"ai_prompt_events_median_per_session": 3,
24+
"ai_prompt_length_avg_per_session": 1233,
25+
"ai_prompt_length_median_per_session": 1100,
26+
"ai_sessions": 5,
27+
"ai_model_line_changes": {
28+
"Sonnet": 1200,
29+
"GPT-4": 134
30+
},
31+
"ai_model_costs": {
32+
"Sonnet": 9.5,
33+
"GPT-4": 2.98
34+
},
35+
"ai_model_breakdown": [
36+
{
37+
"name": "Sonnet",
38+
"lines": 1200,
39+
"cost": 9.5
40+
},
41+
{
42+
"name": "GPT-4",
43+
"lines": 134,
44+
"cost": 2.98
45+
}
46+
],
1147
"holidays": 0,
1248
"status": "ok",
1349
"created_at": "2025-12-31T23:02:40Z",
@@ -627,4 +663,4 @@
627663
"is_category_usage_visible": false,
628664
"is_os_usage_visible": false
629665
}
630-
}
666+
}

0 commit comments

Comments
 (0)