Files
nucleic-remote-ios/NucleicRemote/NucleicRemote/Views/IntelligenceRoute.swift
T

76 lines
3.8 KiB
Swift

import SwiftUI
import NucleicProtocol
/// The line beneath the Intelligence rail: the stop's name and what it resolves to.
///
/// **A readout, not a control.** The Mac lays a transparent menu over the same subtext so the
/// resolved words double as a model/effort picker. That doesn't earn its place here: with the rail
/// on, the rail *is* the control, and a second way to set the same thing — one that silently
/// suspends routing when used — is a contradiction on a surface this small. So this is plain text.
/// Manual model and effort selection remains available exactly where it always was, in the chat's
/// ⋯ menu, and only on a host that isn't routing.
///
/// It still carries three facts the Mac spreads across hover and tooltips, because a phone has
/// neither: the stop's **name** (the rail alone never has to be read for it), the concrete
/// **model · effort** it lands on, and — when quota has closed a lane — the fact that this stop
/// currently can't run.
///
/// The footprint is fixed so a model-name change can't shift the composer's rows under a thumb
/// that's mid-reach for the send button.
struct IntelligenceRouteLine: View {
/// The stop being described — the one under the finger during a drag, else the committed one.
var level: Int
var orchestra: Bool
var catalog: WireIntelligenceCatalog
/// The pair this stop lands on. For an open chat at rest that's the session's *actual* model
/// and effort (ground truth from the host); mid-drag, and in the new-chat composer, it's the
/// route the host would resolve. `nil` shows the stop alone — an empty draft has nothing to
/// route yet, and saying nothing beats promising a model.
var model: String?
var effort: String?
/// The router's own sentence, shown in place of the pair when this stop can't currently run.
var unavailableReason: String?
var modelCatalog: WireModelCatalog = .empty
/// Tracks its own text style, so at accessibility sizes the row grows with the model name it
/// exists to show rather than clipping it.
@ScaledMetric(relativeTo: .caption) private var rowHeight: CGFloat = 16
private var stopName: String {
if orchestra { return catalog.orchestraDisplayName }
let projected = catalog.displayName(forLevel: level)
return projected.isEmpty
? (IntelligenceLevel(rawValue: level)?.displayName ?? "") : projected
}
/// "Balanced · Sonnet 5 · High", or just the stop before anything resolves. Orchestra states
/// its mode rather than a pair — the supervisor model isn't the interesting fact there.
private var summary: String {
if orchestra { return "\(stopName) · parallel subagents" }
if let unavailableReason { return "\(stopName) · \(unavailableReason)" }
guard let model, let effort else { return stopName }
return "\(stopName) · \(modelCatalog.displayName(model)) · "
+ modelCatalog.effortDisplayName(effort).localizedCapitalized
}
var body: some View {
Text(summary)
.font(.caption.weight(.medium))
.foregroundStyle(lineStyle)
.lineLimit(1)
.truncationMode(.middle)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: rowHeight)
// One element with the rail, not a second stop in the VoiceOver order: this restates
// what the rail's own value already says, and a swipe onto a duplicate readout is pure
// friction. The rail is the thing to focus.
.accessibilityHidden(true)
}
private var lineStyle: AnyShapeStyle {
if unavailableReason != nil { return AnyShapeStyle(.orange) }
if orchestra { return AnyShapeStyle(Palette.orchestra) }
return AnyShapeStyle(.secondary)
}
}