spb/zyquo-router Public MIT
One local endpoint, every AI provider — a private OpenAI-compatible LLM gateway for your Mac (170 models, 12 providers).
Swift 95.7%
Python 2.3%
Shell 1.2%
Makefile 0.9%
1//2// RequestsView.swift3// Zyquo Router4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Live traffic: streaming log table (filters, pause/clear, export) and a9// detail inspector with redacted-by-default bodies (explicit per-session10// reveal) and the timing waterfall.11//1213import SwiftUI14import UniformTypeIdentifiers1516struct RequestsView: View {17 @EnvironmentObject private var server: ServerController1819 @State private var entries: [RequestLogEntry] = []20 @State private var revision = -121 @State private var paused = false22 @State private var filterText = ""23 @State private var providerFilter: ProviderID?24 @State private var errorsOnly = false25 @State private var selectedID: UUID?26 @AppStorage("logBodiesRevealed") private var bodiesRevealed = false27 @FocusState private var filterFocused: Bool2829 private let refresh = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()3031 private var filtered: [RequestLogEntry] {32 entries.reversed().filter { entry in33 if let providerFilter, entry.provider != providerFilter { return false }34 if errorsOnly, entry.status < 400 { return false }35 if !filterText.isEmpty,36 !entry.namespacedModelID.localizedCaseInsensitiveContains(filterText) { return false }37 return true38 }39 }4041 var body: some View {42 VStack(alignment: .leading, spacing: 0) {43 toolbar44 ZyquoHairline()45 if filtered.isEmpty {46 EmptyState(47 systemImage: "antenna.radiowaves.left.and.right",48 title: entries.isEmpty ? "No traffic yet" : "Nothing matches the filters",49 message: entries.isEmpty50 ? (server.isRunning51 ? "Requests routed through \(server.endpointURL) will stream in here live."52 : "Start the server, then point any OpenAI client at the endpoint.")53 : "Adjust or clear the filters above."54 )55 } else {56 HSplitView {57 table58 .frame(minWidth: 460)59 if let selected = filtered.first(where: { $0.id == selectedID }) {60 RequestDetailPane(entry: selected, bodiesRevealed: $bodiesRevealed)61 .frame(minWidth: 320)62 }63 }64 }65 }66 .onReceive(refresh) { _ in67 guard !paused else { return }68 let log = server.requestLog69 Task {70 let newRevision = await log.revision71 if newRevision != revision {72 revision = newRevision73 entries = await log.entries74 }75 }76 }77 }7879 private var toolbar: some View {80 HStack(spacing: ZyquoSpacing.sm) {81 SectionHeader(title: "Requests")82 Spacer()83 Toggle("Errors only", isOn: $errorsOnly)84 .toggleStyle(.checkbox)85 .font(ZyquoFont.caption)86 Picker("", selection: $providerFilter) {87 Text("All providers").tag(ProviderID?.none)88 ForEach(ProviderID.builtIn) { provider in89 Text(provider.displayName).tag(ProviderID?.some(provider))90 }91 }92 .labelsHidden()93 .frame(width: 150)94 TextField("Filter by model… (⌘F)", text: $filterText)95 .textFieldStyle(.roundedBorder)96 .frame(width: 190)97 .focused($filterFocused)98 Button {99 paused.toggle()100 } label: {101 Image(systemName: paused ? "play.fill" : "pause.fill")102 }103 .help(paused ? "Resume live updates" : "Pause live updates")104 Button {105 let log = server.requestLog106 Task {107 await log.clear()108 entries = []109 selectedID = nil110 }111 } label: {112 Image(systemName: "trash")113 }114 .help("Clear the log")115 Button {116 exportLog()117 } label: {118 Image(systemName: "square.and.arrow.up")119 }120 .help("Export log metadata as JSON")121 }122 .padding(.horizontal, ZyquoMetrics.contentInset)123 .padding(.vertical, ZyquoSpacing.sm)124 .background(125 // ⌘F routes here from the app menu.126 Button("") { filterFocused = true }127 .keyboardShortcut("f", modifiers: .command)128 .hidden()129 )130 }131132 private var table: some View {133 ScrollView {134 LazyVStack(spacing: 0) {135 ForEach(filtered) { entry in136 RequestRow(entry: entry, selected: entry.id == selectedID)137 .contentShape(Rectangle())138 .onTapGesture {139 selectedID = selectedID == entry.id ? nil : entry.id140 }141 ZyquoHairline()142 .padding(.leading, ZyquoMetrics.contentInset)143 }144 }145 }146 }147148 private func exportLog() {149 let log = server.requestLog150 Task {151 let data = await log.exportJSON()152 let panel = NSSavePanel()153 panel.allowedContentTypes = [.json]154 panel.nameFieldStringValue = "zyquo-router-log.json"155 if panel.runModal() == .OK, let url = panel.url {156 try? data.write(to: url)157 }158 }159 }160}161162// MARK: - Row163164private struct RequestRow: View {165 let entry: RequestLogEntry166 let selected: Bool167 @State private var hovering = false168169 var body: some View {170 HStack(spacing: ZyquoSpacing.sm) {171 Text(entry.date, format: .dateTime.hour().minute().second())172 .font(ZyquoFont.mono(size: 11))173 .foregroundStyle(ZyquoColor.textSecondary)174 .frame(width: 64, alignment: .leading)175176 HStack(spacing: ZyquoSpacing.xs) {177 Circle()178 .fill(ZyquoColor.providerHue(entry.provider))179 .frame(width: 6, height: 6)180 Text(entry.namespacedModelID)181 .font(ZyquoFont.mono(size: 11.5))182 .foregroundStyle(ZyquoColor.textPrimary)183 .lineLimit(1)184 .truncationMode(.middle)185 if entry.streamed {186 CapabilityBadge(label: "SSE", tint: ZyquoColor.accent)187 }188 if entry.usageEstimated {189 CapabilityBadge(label: "EST", tint: ZyquoColor.warning)190 }191 }192 .frame(maxWidth: .infinity, alignment: .leading)193194 Text("\(entry.status)")195 .font(ZyquoFont.mono(size: 11, weight: .semibold))196 .foregroundStyle(entry.status < 400 ? ZyquoColor.success : ZyquoColor.danger)197 .frame(width: 36, alignment: .trailing)198199 Text(String(format: "%.2fs", entry.latency))200 .font(ZyquoFont.mono(size: 11))201 .foregroundStyle(ZyquoColor.textSecondary)202 .frame(width: 56, alignment: .trailing)203204 Text("\(entry.usage.inputTokens)→\(entry.usage.outputTokens)")205 .font(ZyquoFont.mono(size: 11))206 .foregroundStyle(ZyquoColor.textSecondary)207 .frame(width: 84, alignment: .trailing)208209 Text(costText)210 .font(ZyquoFont.mono(size: 11))211 .foregroundStyle(ZyquoColor.textSecondary)212 .frame(width: 58, alignment: .trailing)213 }214 .padding(.horizontal, ZyquoMetrics.contentInset)215 .padding(.vertical, 6)216 .background(selected ? ZyquoColor.accentSubtle : (hovering ? ZyquoColor.surfaceSecondary : .clear))217 .onHover { hover in218 withAnimation(ZyquoMotion.hover) { hovering = hover }219 }220 }221222 private var costText: String {223 guard let cost = entry.estimatedCost, cost > 0 else { return "—" }224 return cost < 0.01 ? "<$0.01" : String(format: "$%.2f", cost)225 }226}227228// MARK: - Detail inspector229230private struct RequestDetailPane: View {231 let entry: RequestLogEntry232 @Binding var bodiesRevealed: Bool233234 var body: some View {235 ScrollView {236 VStack(alignment: .leading, spacing: ZyquoSpacing.md) {237 Text(entry.namespacedModelID)238 .font(ZyquoFont.mono(size: 13, weight: .medium))239 .foregroundStyle(ZyquoColor.textPrimary)240 .textSelection(.enabled)241242 waterfall243244 if let error = entry.errorMessage {245 Label(error, systemImage: "xmark.octagon")246 .font(ZyquoFont.body())247 .foregroundStyle(ZyquoColor.danger)248 .textSelection(.enabled)249 }250251 Toggle("Reveal request/response bodies (this session)", isOn: $bodiesRevealed)252 .toggleStyle(.switch)253 .controlSize(.small)254 .font(ZyquoFont.caption)255256 bodySection(title: "REQUEST", body: entry.requestBody)257 bodySection(title: "RESPONSE", body: entry.responseBody)258 }259 .padding(ZyquoSpacing.md)260 }261 .background(ZyquoColor.surface)262 }263264 private var waterfall: some View {265 VStack(alignment: .leading, spacing: ZyquoSpacing.xxs) {266 caption("TIMING")267 GeometryReader { proxy in268 let total = max(entry.latency, 0.001)269 let ttfb = min(entry.upstreamTTFB ?? entry.latency, total)270 HStack(spacing: 1) {271 RoundedRectangle(cornerRadius: 2)272 .fill(ZyquoColor.graphite.opacity(0.55))273 .frame(width: max(proxy.size.width * (ttfb / total), 2))274 RoundedRectangle(cornerRadius: 2)275 .fill(ZyquoColor.accent)276 .frame(maxWidth: .infinity)277 }278 }279 .frame(height: 8)280 HStack {281 Text(entry.upstreamTTFB.map { String(format: "upstream TTFB %.0f ms", $0 * 1000) } ?? "buffered call")282 Spacer()283 Text(String(format: "total %.2f s", entry.latency))284 }285 .font(ZyquoFont.mono(size: 10.5))286 .foregroundStyle(ZyquoColor.textSecondary)287 }288 }289290 @ViewBuilder291 private func bodySection(title: String, body text: String) -> some View {292 VStack(alignment: .leading, spacing: ZyquoSpacing.xxs) {293 HStack {294 caption(title)295 Spacer()296 if bodiesRevealed, !text.isEmpty {297 CopyButton(value: text)298 }299 }300 Group {301 if !bodiesRevealed {302 Text("Redacted — enable reveal above to inspect.")303 .font(ZyquoFont.caption)304 .foregroundStyle(ZyquoColor.textTertiary)305 .frame(maxWidth: .infinity, alignment: .leading)306 } else {307 Text(text.isEmpty ? "(empty)" : text)308 .font(ZyquoFont.mono(size: 10.5))309 .foregroundStyle(ZyquoColor.textPrimary)310 .textSelection(.enabled)311 .frame(maxWidth: .infinity, alignment: .leading)312 }313 }314 .padding(ZyquoSpacing.xs)315 .background(316 RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous)317 .fill(ZyquoColor.surfaceSecondary)318 )319 }320 }321322 private func caption(_ text: String) -> some View {323 Text(text)324 .font(.system(size: 9.5, weight: .semibold))325 .foregroundStyle(ZyquoColor.textTertiary)326 .kerning(0.4)327 }328}329