Skip to content

Commit 7c9159a

Browse files
DavidsonGomesclaude
andcommitted
release: v0.18.1 — AI image creator cost tracking in dashboard
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d1be795 commit 7c9159a

File tree

7 files changed

+150
-10
lines changed

7 files changed

+150
-10
lines changed

.claude/skills/ai-image-creator/scripts/generate-image.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -896,11 +896,16 @@ def process_transparent(input_path: Path, output_path: Path) -> None:
896896

897897

898898
def get_costs_path() -> Path:
899-
"""Get project-level costs file path.
899+
"""Get workspace-level costs file path.
900900
901901
Returns:
902-
Path to .ai-image-creator/costs.json in current working directory.
902+
Path to ADWs/logs/ai-image-creator-costs.json in workspace root.
903+
Falls back to CWD if workspace root is not found.
903904
"""
905+
workspace_root = Path(__file__).resolve().parent.parent.parent.parent.parent
906+
logs_dir = workspace_root / "ADWs" / "logs"
907+
if logs_dir.is_dir():
908+
return logs_dir / "ai-image-creator-costs.json"
904909
return Path.cwd() / ".ai-image-creator" / "costs.json"
905910

906911

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.18.1] - 2026-04-12
9+
10+
### Added
11+
12+
- **AI Image Creator cost tracking in dashboard** — new "Geração de Imagens" section in Costs page showing per-image model, provider, tokens, size, and elapsed time with totals
13+
- **Image costs API endpoint**`GET /api/routines/image-costs` reads cost entries from `ADWs/logs/ai-image-creator-costs.json`
14+
15+
### Changed
16+
17+
- **AI Image Creator costs path** — cost logs now saved to `ADWs/logs/ai-image-creator-costs.json` (workspace-level) instead of `.ai-image-creator/costs.json` (project-level)
18+
819
## [0.18.0] - 2026-04-12
920

1021
### Added

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@evoapi/evo-nexus",
3-
"version": "0.18.0",
3+
"version": "0.18.1",
44
"description": "Unofficial open source toolkit for Claude Code — AI-powered business operating system",
55
"keywords": [
66
"claude-code",

dashboard/backend/routes/routines.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,35 @@ def list_adws():
8282
pass
8383
scripts.append({"name": f.stem, "file": f.name, "description": doc})
8484
return jsonify(scripts)
85+
86+
87+
@bp.route("/api/routines/image-costs")
88+
def get_image_costs():
89+
"""Return AI image generation cost entries."""
90+
costs_path = LOGS_DIR / "ai-image-creator-costs.json"
91+
content = safe_read(costs_path)
92+
if not content:
93+
return jsonify({"entries": [], "totals": {}})
94+
try:
95+
entries = json.loads(content)
96+
except json.JSONDecodeError:
97+
return jsonify({"entries": [], "totals": {}})
98+
99+
total_tokens = 0
100+
total_seconds = 0.0
101+
total_bytes = 0
102+
for e in entries:
103+
tokens = e.get("token_usage", {})
104+
total_tokens += tokens.get("total_tokens", 0)
105+
total_seconds += e.get("elapsed_seconds", 0)
106+
total_bytes += e.get("size_bytes", 0)
107+
108+
return jsonify({
109+
"entries": entries,
110+
"totals": {
111+
"count": len(entries),
112+
"total_tokens": total_tokens,
113+
"total_seconds": round(total_seconds, 1),
114+
"total_bytes": total_bytes,
115+
},
116+
})

dashboard/frontend/src/pages/Costs.tsx

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useState } from 'react'
2-
import { DollarSign, Zap, Activity, Calculator, type LucideIcon } from 'lucide-react'
2+
import { DollarSign, Zap, Activity, Calculator, Image, type LucideIcon } from 'lucide-react'
33
import { api } from '../lib/api'
44
import {
55
LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
@@ -91,15 +91,53 @@ function SkeletonCard() {
9191
)
9292
}
9393

94+
interface ImageCostEntry {
95+
timestamp: string
96+
model: string
97+
provider: string
98+
mode: string
99+
output_file: string
100+
size_bytes: number
101+
elapsed_seconds: number
102+
token_usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number }
103+
}
104+
105+
interface ImageCosts {
106+
entries: ImageCostEntry[]
107+
totals: { count: number; total_tokens: number; total_seconds: number; total_bytes: number }
108+
}
109+
110+
function relativeTime(ts: string): string {
111+
try {
112+
const diff = Date.now() - new Date(ts).getTime()
113+
const min = Math.floor(diff / 60000)
114+
if (min < 1) return 'just now'
115+
if (min < 60) return `${min}m ago`
116+
const hr = Math.floor(min / 60)
117+
if (hr < 24) return `${hr}h ago`
118+
return `${Math.floor(hr / 24)}d ago`
119+
} catch { return ts }
120+
}
121+
122+
function formatBytes(b: number): string {
123+
if (b < 1024) return `${b} B`
124+
if (b < 1024 * 1024) return `${(b / 1024).toFixed(0)} KB`
125+
return `${(b / (1024 * 1024)).toFixed(1)} MB`
126+
}
127+
94128
export default function Costs() {
95129
const [data, setData] = useState<CostData | null>(null)
130+
const [imageCosts, setImageCosts] = useState<ImageCosts | null>(null)
96131
const [loading, setLoading] = useState(true)
97132

98133
useEffect(() => {
99-
api.get('/costs')
100-
.then((raw) => setData(normalizeCostData(raw)))
101-
.catch(() => setData(null))
102-
.finally(() => setLoading(false))
134+
Promise.all([
135+
api.get('/costs').catch(() => null),
136+
api.get('/routines/image-costs').catch(() => null),
137+
]).then(([costRaw, imgRaw]) => {
138+
if (costRaw) setData(normalizeCostData(costRaw))
139+
if (imgRaw) setImageCosts(imgRaw)
140+
}).finally(() => setLoading(false))
103141
}, [])
104142

105143
if (loading) {
@@ -270,6 +308,60 @@ export default function Costs() {
270308
</table>
271309
</div>
272310
</div>
311+
312+
{/* Image Generation Costs */}
313+
{imageCosts && imageCosts.entries.length > 0 && (
314+
<div className="bg-[#161b22] border border-[#21262d] rounded-2xl overflow-hidden mt-6 transition-all duration-300 hover:shadow-[0_0_32px_rgba(0,255,167,0.04)]">
315+
<div className="p-5 border-b border-[#21262d] flex items-center justify-between">
316+
<h2 className="text-base font-semibold text-[#e6edf3] flex items-center gap-2.5">
317+
<div className="flex items-center justify-center w-7 h-7 rounded-lg bg-[#F472B6]/10 border border-[#F472B6]/20">
318+
<Image size={14} className="text-[#F472B6]" />
319+
</div>
320+
Image Generation
321+
</h2>
322+
<div className="flex items-center gap-4 text-[11px] text-[#667085]">
323+
<span>{imageCosts.totals.count} images</span>
324+
<span>{imageCosts.totals.total_tokens.toLocaleString()} tokens</span>
325+
<span>{formatBytes(imageCosts.totals.total_bytes)}</span>
326+
<span>{imageCosts.totals.total_seconds}s total</span>
327+
</div>
328+
</div>
329+
<div className="overflow-x-auto">
330+
<table className="w-full text-sm">
331+
<thead>
332+
<tr className="text-[#667085] text-[11px] uppercase tracking-wider font-medium">
333+
<th className="text-left p-4 pb-3">Model</th>
334+
<th className="text-left p-4 pb-3">Provider</th>
335+
<th className="text-left p-4 pb-3">Output</th>
336+
<th className="text-right p-4 pb-3">Tokens</th>
337+
<th className="text-right p-4 pb-3">Size</th>
338+
<th className="text-right p-4 pb-3">Time</th>
339+
<th className="text-right p-4 pb-3">When</th>
340+
</tr>
341+
</thead>
342+
<tbody>
343+
{[...imageCosts.entries].reverse().map((e, i) => (
344+
<tr key={i} className="border-t border-[#21262d]/60 hover:bg-white/[0.02] transition-colors group">
345+
<td className="p-4">
346+
<code className="text-[11px] text-[#F472B6] font-mono bg-[#F472B6]/8 px-2 py-0.5 rounded border border-[#F472B6]/15">
347+
{e.model.split('/').pop()}
348+
</code>
349+
</td>
350+
<td className="p-4 text-[#8b949e] text-[13px]">{e.provider} <span className="text-[#667085]">({e.mode})</span></td>
351+
<td className="p-4 text-[#e6edf3] text-[13px] font-medium group-hover:text-white truncate max-w-[200px]" title={e.output_file}>
352+
{e.output_file.split('/').pop()}
353+
</td>
354+
<td className="p-4 text-right text-[#8b949e] tabular-nums text-[13px]">{e.token_usage.total_tokens.toLocaleString()}</td>
355+
<td className="p-4 text-right text-[#8b949e] tabular-nums text-[13px]">{formatBytes(e.size_bytes)}</td>
356+
<td className="p-4 text-right text-[#667085] tabular-nums text-[13px]">{e.elapsed_seconds.toFixed(1)}s</td>
357+
<td className="p-4 text-right text-[#667085] text-[13px] whitespace-nowrap">{relativeTime(e.timestamp)}</td>
358+
</tr>
359+
))}
360+
</tbody>
361+
</table>
362+
</div>
363+
</div>
364+
)}
273365
</div>
274366
)
275367
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "evo-nexus"
3-
version = "0.18.0"
3+
version = "0.18.1"
44
description = "Unofficial open source toolkit for Claude Code — AI-powered business operating system"
55
requires-python = ">=3.10"
66
dependencies = [

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)