SPB Git

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%
8.0 KB · 216 lines swift
Raw Blame History
1//2//  SettingsView.swift3//  Zyquo Router4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Native settings tabs (720×540): Server, Logging, Usage & Pricing,9//  Appearance, Shortcuts, Advanced. Server + Appearance are fully live in10//  Phase 4; the rest fill in with Phase 6 features.11//1213import ServiceManagement14import SwiftUI15import UniformTypeIdentifiers1617struct SettingsView: View {18    var body: some View {19        TabView {20            ServerSettings()21                .tabItem { Label("Server", systemImage: "server.rack") }22            LoggingSettings()23                .tabItem { Label("Logging", systemImage: "doc.text") }24            UsageSettings()25                .tabItem { Label("Usage & Pricing", systemImage: "dollarsign.circle") }26            AppearanceSettings()27                .tabItem { Label("Appearance", systemImage: "paintpalette") }28            ShortcutsSettings()29                .tabItem { Label("Shortcuts", systemImage: "keyboard") }30            AdvancedSettings()31                .tabItem { Label("Advanced", systemImage: "gearshape.2") }32        }33        .frame(width: ZyquoMetrics.settingsWidth, height: ZyquoMetrics.settingsHeight)34    }35}3637private struct ServerSettings: View {38    @EnvironmentObject private var server: ServerController39    @AppStorage("autoStartServer") private var autoStart = false40    @AppStorage("keepServingWhenClosed") private var keepServing = true41    @AppStorage("menuBarExtraEnabled") private var menuBarExtra = true42    @State private var launchAtLogin = SMAppService.mainApp.status == .enabled4344    var body: some View {45        Form {46            TextField("Default port:", value: $server.port, format: .number.grouping(.never))47                .frame(width: 200)48            Picker("Bind:", selection: $server.bindLAN) {49                Text("Localhost only").tag(false)50                Text("LAN (requires a local API key)").tag(true)51            }52            .frame(width: 340)53            Toggle("Start the server when the app launches", isOn: $autoStart)54            Toggle("Keep serving when the window is closed", isOn: $keepServing)55            Toggle("Launch Zyquo Router at login", isOn: $launchAtLogin)56                .onChange(of: launchAtLogin) { enabled in57                    do {58                        if enabled {59                            try SMAppService.mainApp.register()60                        } else {61                            try SMAppService.mainApp.unregister()62                        }63                    } catch {64                        launchAtLogin = SMAppService.mainApp.status == .enabled65                    }66                }67            Toggle("Show the menu bar extra", isOn: $menuBarExtra)68            Text("Request body limit: 32 MB · streaming timeout: 15 min")69                .font(ZyquoFont.caption)70                .foregroundStyle(ZyquoColor.textSecondary)71        }72        .padding(ZyquoSpacing.xl)73    }74}7576private struct LoggingSettings: View {77    @AppStorage("logBodiesRevealed") private var reveal = false7879    var body: some View {80        Form {81            Toggle("Reveal request/response bodies in the inspector (this session)", isOn: $reveal)82            Text("Bodies are redacted by default. Metadata (model, status, latency, tokens, cost) is always recorded; provider keys are never logged.")83                .font(ZyquoFont.caption)84                .foregroundStyle(ZyquoColor.textSecondary)85        }86        .padding(ZyquoSpacing.xl)87    }88}8990private struct UsageSettings: View {91    @EnvironmentObject private var catalog: ModelCatalog92    @EnvironmentObject private var server: ServerController93    @State private var confirmingReset = false9495    var body: some View {96        Form {97            Text("Costs are estimated from the catalog's per-model pricing (\(catalog.all.count) models). Usage counters reset daily at midnight.")98                .font(ZyquoFont.body())99                .foregroundStyle(ZyquoColor.textSecondary)100            Button("Reset usage counters…") { confirmingReset = true }101                .confirmationDialog("Clear all recorded usage and the request log?", isPresented: $confirmingReset) {102                    Button("Reset", role: .destructive) {103                        let log = server.requestLog104                        Task { await log.clear() }105                    }106                }107        }108        .padding(ZyquoSpacing.xl)109    }110}111112private struct AppearanceSettings: View {113    @EnvironmentObject private var appearance: AppearanceStore114115    var body: some View {116        Form {117            Picker("Theme:", selection: $appearance.themeMode) {118                ForEach(ThemeMode.allCases) { mode in119                    Text(mode.displayName).tag(mode)120                }121            }122            .pickerStyle(.segmented)123            .frame(width: 320)124125            Picker("Accent:", selection: $appearance.accent) {126                ForEach(AccentChoice.allCases) { accent in127                    Text(accent.displayName).tag(accent)128                }129            }130            .frame(width: 320)131132            Slider(value: $appearance.fontSize, in: 12...16, step: 0.5) {133                Text("Font size: \(appearance.fontSize, format: .number.precision(.fractionLength(1)))pt")134            }135            .frame(width: 380)136        }137        .padding(ZyquoSpacing.xl)138    }139}140141private struct ShortcutsSettings: View {142    var body: some View {143        Form {144            shortcut("⌘R", "Start / stop the server")145            shortcut("⌘1–6", "Switch sections")146            shortcut("⌘⇧C", "Copy endpoint URL")147            shortcut("⌘K", "Command palette")148            shortcut("⌘F", "Filter requests")149            shortcut("⌘⏎", "Send in Playground")150        }151        .padding(ZyquoSpacing.xl)152    }153154    private func shortcut(_ keys: String, _ label: String) -> some View {155        HStack {156            Text(keys)157                .font(ZyquoFont.mono(size: 12, weight: .medium))158                .foregroundStyle(ZyquoColor.textPrimary)159                .frame(width: 70, alignment: .leading)160            Text(label)161                .font(ZyquoFont.body())162                .foregroundStyle(ZyquoColor.textSecondary)163        }164    }165}166167private struct AdvancedSettings: View {168    @EnvironmentObject private var routerConfig: RouterConfigStore169    @State private var importError: String?170171    var body: some View {172        Form {173            Button("Reveal data folder in Finder") {174                NSWorkspace.shared.activateFileViewerSelecting([PersistenceService.shared.rootDirectory])175            }176            HStack {177                Button("Export config…") { exportConfig() }178                Button("Import config…") { importConfig() }179            }180            if let importError {181                Text(importError)182                    .font(ZyquoFont.caption)183                    .foregroundStyle(ZyquoColor.danger)184            }185            Text("Config export covers aliases, fallback chains, disabled models, and favorites — provider keys stay in the encrypted vault (vault.zq) and are never exported in plaintext.")186                .font(ZyquoFont.caption)187                .foregroundStyle(ZyquoColor.textSecondary)188        }189        .padding(ZyquoSpacing.xl)190    }191192    private func exportConfig() {193        guard let data = try? routerConfig.exportData() else { return }194        let panel = NSSavePanel()195        panel.allowedContentTypes = [.json]196        panel.nameFieldStringValue = "zyquo-router-config.json"197        if panel.runModal() == .OK, let url = panel.url {198            try? data.write(to: url)199        }200    }201202    private func importConfig() {203        let panel = NSOpenPanel()204        panel.allowedContentTypes = [.json]205        panel.allowsMultipleSelection = false206        if panel.runModal() == .OK, let url = panel.url {207            do {208                try routerConfig.importData(try Data(contentsOf: url))209                importError = nil210            } catch {211                importError = "Not a valid Zyquo Router config file."212            }213        }214    }215}216