Modell · Dienst · Dauer · Tokens · Mana · ≈ Strom (System-Styling). Pendant zur Web-Komponente @mana/shared-ui <AiActionMeta>. Energie als Schätzung markiert. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
88 lines
2.9 KiB
Swift
88 lines
2.9 KiB
Swift
import ManaLLMShared
|
|
import SwiftUI
|
|
|
|
/// Eine kompakte, gedämpfte Zeile, die zeigt, was ein KI-Aufruf gekostet
|
|
/// hat: Modell · Dienst · Dauer · Tokens · Mana · ≈ Strom. Geteilte,
|
|
/// app-agnostische Anzeige (System-Styling) — Pendant zur Web-Komponente
|
|
/// `<AiActionMeta>` in `@mana/shared-ui`. Die Energie ist bewusst als
|
|
/// Schätzung markiert (≈). SOT: `mana/docs/AI_TRANSPARENCY.md`.
|
|
public struct ManaAiActionMetaView: View {
|
|
private let meta: ManaAiMeta?
|
|
|
|
public init(meta: ManaAiMeta?) {
|
|
self.meta = meta
|
|
}
|
|
|
|
public var body: some View {
|
|
if let meta {
|
|
Text(Self.line(meta))
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
.accessibilityLabel("KI-Verbrauch: \(Self.line(meta))")
|
|
}
|
|
}
|
|
|
|
/// Baut die einzeilige Verbrauchs-Zusammenfassung.
|
|
static func line(_ m: ManaAiMeta) -> String {
|
|
var parts: [String] = [modelName(m.modelResolved), service(m)]
|
|
if let ms = m.durationMs {
|
|
parts.append("\(decimal(ms / 1000, digits: 1)) s")
|
|
}
|
|
let tokens = (m.tokensIn ?? 0) + (m.tokensOut ?? 0)
|
|
if tokens > 0 {
|
|
parts.append("\(tokens) Tokens")
|
|
}
|
|
if let credits = m.creditsCharged {
|
|
parts.append("\(credits) Mana")
|
|
}
|
|
// Strom erscheint IMMER, damit „0 Mana" nie allein als einzige
|
|
// Kosten-Aussage steht (Compliance-Invariante, AI_TRANSPARENCY.md).
|
|
parts.append(energyText(m))
|
|
return parts.joined(separator: " · ")
|
|
}
|
|
|
|
/// Folgenkosten-Aussage zum Strom. `(geschätzt)` ist sichtbar (nicht nur
|
|
/// im Accessibility-Label), und es wird mit signifikanten Stellen
|
|
/// gerundet — eine grobe Schätzung darf keine Hundertstel vortäuschen.
|
|
static func energyText(_ m: ManaAiMeta) -> String {
|
|
if let wh = m.energyWhEst {
|
|
return "≈ \(formatWh(wh)) Wh (geschätzt)"
|
|
}
|
|
if m.energyBasis == "on-device" || m.source == "local" || m.source == "byok" {
|
|
return "Strom von deinem Gerät"
|
|
}
|
|
return "Strom nicht beziffert"
|
|
}
|
|
|
|
/// Wattstunden mit signifikanten Stellen: ≥ 1 Wh ganzzahlig, < 1 Wh
|
|
/// eine Nachkommastelle.
|
|
static func formatWh(_ wh: Double) -> String {
|
|
decimal(wh, digits: wh >= 1 ? 0 : 1)
|
|
}
|
|
|
|
/// `ollama/pixtral:12b` → `pixtral:12b`.
|
|
static func modelName(_ model: String) -> String {
|
|
guard let slash = model.firstIndex(of: "/") else { return model }
|
|
return String(model[model.index(after: slash)...])
|
|
}
|
|
|
|
/// Ehrliches Dienst-Label: self-hosted vs. welcher Anbieter, plus Tier.
|
|
static func service(_ m: ManaAiMeta) -> String {
|
|
let provider: String
|
|
switch m.provider {
|
|
case "ollama": provider = "eigene Hardware"
|
|
case "unknown", "": provider = "Server"
|
|
default: provider = m.provider
|
|
}
|
|
return m.source == "metered" ? "\(provider) · bezahlt" : provider
|
|
}
|
|
|
|
static func decimal(_ value: Double, digits: Int) -> String {
|
|
let f = NumberFormatter()
|
|
f.locale = Locale(identifier: "de_DE")
|
|
f.minimumFractionDigits = 0
|
|
f.maximumFractionDigits = digits
|
|
return f.string(from: NSNumber(value: value)) ?? String(value)
|
|
}
|
|
}
|