codeburn/mac/Sources/CodeBurnMenubar/Views/HeroSection.swift
Resham Joshi 389c14ce5e
fix(ui): mark estimated headline totals with ~ on every hero (#1722)
#1657 put the ~ mark on estimated rows, but the headline total stayed bare on
the desktop hero, the mac hero, the Windows hero and tray text, and GNOME.
Each now applies the same 1% rule to the provider split's estimated portion
(the desktop helper moves from the VS Code summary into lib/format). Combined
device totals carry no estimated split and stay unmarked.
2026-10-10 07:05:58 +02:00

248 lines
10 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(totals.outputTokens)))
}
.font(.system(size: 11))
.monospacedDigit()
.foregroundStyle(.secondary)
HStack(spacing: 2) {
Image(systemName: "arrow.down")
.font(.system(size: 9, weight: .semibold))
Text(formatTokens(Double(totals.inputTokens)))
}
.font(.system(size: 10.5))
.monospacedDigit()
.foregroundStyle(.tertiary)
} else {
Text(totals.calls == 1 ? L("%@ call", totals.calls.asThousandsSeparated()) : L("%@ calls", totals.calls.asThousandsSeparated()))
.font(.system(size: 11))
.monospacedDigit()
.foregroundStyle(.secondary)
Text(combinedUsage != nil
? SessionCountLabel.combinedText
: SessionCountLabel.text(sessions: totals.sessions, basis: totals.sessionCountBasis))
.font(.system(size: 10.5))
.monospacedDigit()
.foregroundStyle(.tertiary)
.help(SessionCountLabel.helpText(combined: combinedUsage != nil, basis: totals.sessionCountBasis))
}
}
}
if !store.isDayMode,
store.selectedPeriod == .today,
store.shouldShowDailyBudgetWarning {
HStack(spacing: 4) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 10))
Text(L("Daily budget of %@ exceeded", store.dailyBudgetLabel))
.font(.system(size: 11, weight: .medium))
}
.foregroundStyle(.orange)
.padding(.top, 2)
}
if let usage = combinedUsage {
CombinedDeviceBreakdown(usage: usage, formatTokens: formatTokens)
} else if store.activeScope == .combined, store.lastError != nil {
HStack(spacing: 4) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 10))
Text(L("Combined unavailable · showing local"))
.font(.system(size: 11, weight: .medium))
}
.foregroundStyle(.secondary)
}
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(totals.totalTokens)
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)
}
let shown = totals.cost.asCurrency()
// The combined aggregate carries no estimated split, so it is never marked.
guard combinedUsage == nil else { return shown }
let estimated = store.payload.current.providerDetails.reduce(0.0) { $0 + ($1.estimatedCostUSD ?? 0) }
return (isEstimatedCost(totals.cost, estimated, shown: shown) ? "~" : "") + shown
}
private var combinedUsage: CombinedUsage? {
guard store.activeScope == .combined else { return nil }
return store.payload.combined
}
private var totals: HeroTotals {
HeroTotals(payload: store.payload, activeScope: store.activeScope)
}
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 combinedUsage != nil {
return L("Combined · %@", 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? {
guard combinedUsage == nil else { return nil }
let savings = store.payload.current.localModelSavings.totalUSD
guard savings > 0 else { return nil }
return L("Saved %@ with local models", savings.asCurrency())
}
private var todayDate: String {
let formatter = DateFormatter()
// Localized template, not a fixed pattern: zh-Hans wants "9月11日周五",
// not "Fri Sep 11".
formatter.setLocalizedDateFormatFromTemplate("EEE MMM d")
return formatter.string(from: Date())
}
}
struct HeroTotals: Equatable {
let cost: Double
let calls: Int
let sessions: Int
let sessionCountBasis: String?
let inputTokens: Int
let outputTokens: Int
let totalTokens: Int
init(cost: Double, calls: Int, sessions: Int, inputTokens: Int, outputTokens: Int, totalTokens: Int, sessionCountBasis: String? = nil) {
self.cost = cost
self.calls = calls
self.sessions = sessions
self.sessionCountBasis = sessionCountBasis
self.inputTokens = inputTokens
self.outputTokens = outputTokens
self.totalTokens = totalTokens
}
init(payload: MenubarPayload, activeScope: MenubarScope) {
if activeScope == .combined, let combined = payload.combined?.combined {
// Keep the numeric wire sum for compatibility. Combined-scope UI
// must not present it as a unique or lower-bound count.
self.init(
cost: combined.cost,
calls: combined.calls,
sessions: combined.sessions,
inputTokens: combined.inputTokens,
outputTokens: combined.outputTokens,
totalTokens: combined.inputTokens + combined.outputTokens
)
return
}
let current = payload.current
self.init(
cost: current.cost,
calls: current.calls,
sessions: current.sessions,
inputTokens: current.inputTokens,
outputTokens: current.outputTokens,
totalTokens: current.inputTokens + current.outputTokens,
sessionCountBasis: current.sessionCountBasis
)
}
}
private struct CombinedDeviceBreakdown: View {
let usage: CombinedUsage
let formatTokens: (Double) -> String
var body: some View {
VStack(alignment: .leading, spacing: 5) {
HStack(spacing: 4) {
Image(systemName: "desktopcomputer")
.font(.system(size: 10))
Text(L("%1$lld of %2$lld devices", usage.combined.reachableCount, usage.combined.deviceCount))
.font(.system(size: 11, weight: .medium))
}
.foregroundStyle(.secondary)
VStack(spacing: 3) {
ForEach(usage.perDevice, id: \.id) { device in
HStack(spacing: 6) {
Image(systemName: device.error == nil ? "circle.fill" : "exclamationmark.triangle.fill")
.font(.system(size: device.error == nil ? 5 : 9, weight: .semibold))
.foregroundStyle(device.error == nil ? Color.secondary.opacity(0.75) : Theme.semanticWarning)
.frame(width: 10)
Text(device.local ? L("%@ · local", device.name) : device.name)
.font(.system(size: 10.5, weight: .medium))
.lineLimit(1)
.truncationMode(.tail)
Spacer(minLength: 6)
Text(device.error == nil ? device.cost.asCurrency() : L("Unavailable"))
.font(.system(size: 10.5))
.monospacedDigit()
.foregroundStyle(.secondary)
Text(formatTokens(Double(device.totalTokens)))
.font(.system(size: 10))
.monospacedDigit()
.foregroundStyle(.tertiary)
}
}
}
}
}
}