mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-19 05:24:28 +00:00
The daily budget was always measured in dollars, so users tracking the Tokens or Total Tokens metric had no token-based budget alert. Add a separate token-denominated daily budget and route the flame tint and the hero "budget exceeded" banner through one metric-aware check. - New dailyTokenBudget (stored separately from the cost budget so switching metric never reinterprets a threshold). - Centralize the logic on AppStore: isTokenMetric, activeDailyBudget, todayMetricTotal, isOverDailyBudget, dailyBudgetLabel. - Settings shows a token-threshold picker (1M..100M) for token metrics and the existing dollar picker for the cost metric, with metric-aware help text. - Flame tint and the banner now use isOverDailyBudget / dailyBudgetLabel.
126 lines
5.2 KiB
Swift
126 lines
5.2 KiB
Swift
import SwiftUI
|
|
|
|
struct HeroSection: View {
|
|
@Environment(AppStore.self) private var store
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
SectionCaption(text: caption)
|
|
|
|
HStack(alignment: .firstTextBaseline) {
|
|
Text(heroText)
|
|
.font(.system(size: 32, weight: .semibold, design: .rounded))
|
|
.monospacedDigit()
|
|
.tracking(-1)
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [Theme.brandAccent, Theme.brandAccentDeep],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
|
|
Spacer()
|
|
|
|
VStack(alignment: .trailing, spacing: 2) {
|
|
if store.displayMetric == .tokens {
|
|
HStack(spacing: 2) {
|
|
Image(systemName: "arrow.up")
|
|
.font(.system(size: 9, weight: .semibold))
|
|
Text(formatTokens(Double(store.payload.current.outputTokens)))
|
|
}
|
|
.font(.system(size: 11))
|
|
.monospacedDigit()
|
|
.foregroundStyle(.secondary)
|
|
HStack(spacing: 2) {
|
|
Image(systemName: "arrow.down")
|
|
.font(.system(size: 9, weight: .semibold))
|
|
Text(formatTokens(Double(store.payload.current.inputTokens)))
|
|
}
|
|
.font(.system(size: 10.5))
|
|
.monospacedDigit()
|
|
.foregroundStyle(.tertiary)
|
|
} else {
|
|
Text("\(store.payload.current.calls.asThousandsSeparated()) calls")
|
|
.font(.system(size: 11))
|
|
.monospacedDigit()
|
|
.foregroundStyle(.secondary)
|
|
Text("\(store.payload.current.sessions) sessions")
|
|
.font(.system(size: 10.5))
|
|
.monospacedDigit()
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !store.isDayMode,
|
|
store.selectedPeriod == .today,
|
|
store.isOverDailyBudget {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
.font(.system(size: 10))
|
|
Text("Daily budget of \(store.dailyBudgetLabel) exceeded")
|
|
.font(.system(size: 11, weight: .medium))
|
|
}
|
|
.foregroundStyle(.orange)
|
|
.padding(.top, 2)
|
|
}
|
|
|
|
if let savingsCaption {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "leaf.fill")
|
|
.font(.system(size: 10))
|
|
Text(savingsCaption)
|
|
.font(.system(size: 11, weight: .medium))
|
|
}
|
|
.foregroundStyle(.green)
|
|
}
|
|
}
|
|
.padding(.horizontal, 14)
|
|
.padding(.top, 10)
|
|
.padding(.bottom, 12)
|
|
}
|
|
|
|
private var heroText: String {
|
|
if store.displayMetric == .tokens || store.displayMetric == .totalTokens {
|
|
let total = Double(store.payload.current.inputTokens + store.payload.current.outputTokens)
|
|
if total >= 1_000_000_000 { return String(format: "%.2fB tok", total / 1_000_000_000) }
|
|
if total >= 1_000_000 { return String(format: "%.1fM tok", total / 1_000_000) }
|
|
if total >= 1_000 { return String(format: "%.0fK tok", total / 1_000) }
|
|
return String(format: "%.0f tok", total)
|
|
}
|
|
return store.payload.current.cost.asCurrency()
|
|
}
|
|
|
|
private func formatTokens(_ n: Double) -> String {
|
|
if n >= 1_000_000_000 { return String(format: "%.1fB", n / 1_000_000_000) }
|
|
if n >= 1_000_000 { return String(format: "%.1fM", n / 1_000_000) }
|
|
if n >= 1_000 { return String(format: "%.0fK", n / 1_000) }
|
|
return String(format: "%.0f", n)
|
|
}
|
|
|
|
private var caption: String {
|
|
let label = store.payload.current.label.isEmpty ? store.selectedPeriod.rawValue : store.payload.current.label
|
|
if !store.isDayMode && store.selectedPeriod == .today {
|
|
return "\(label) · \(todayDate)"
|
|
}
|
|
return label
|
|
}
|
|
|
|
/// Local-model savings caption shown beneath the hero amount when the
|
|
/// user has mapped any local model to a paid baseline via
|
|
/// `codeburn model-savings`. Kept as a separate line so actual spend
|
|
/// (above) and hypothetical avoided spend (below) never get summed
|
|
/// into a misleading "real cost" by the reader.
|
|
private var savingsCaption: String? {
|
|
let savings = store.payload.current.localModelSavings.totalUSD
|
|
guard savings > 0 else { return nil }
|
|
return "Saved \(savings.asCurrency()) with local models"
|
|
}
|
|
|
|
private var todayDate: String {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "EEE MMM d"
|
|
return formatter.string(from: Date())
|
|
}
|
|
}
|