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// KeysView.swift3// Zyquo Router4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Two tabs: Provider Keys (masked vault fields with per-provider Test —9// identical UX to Zyquo Cloud) and Local API Keys (create/reveal-once/10// revoke zyquo-sk tokens).11//1213import SwiftUI1415struct KeysView: View {16 @State private var tab: Tab = .provider1718 enum Tab: String, CaseIterable, Identifiable {19 case provider = "Provider Keys"20 case local = "Local API Keys"21 var id: String { rawValue }22 }2324 var body: some View {25 VStack(alignment: .leading, spacing: 0) {26 HStack {27 SectionHeader(title: "Keys")28 Spacer()29 Picker("", selection: $tab) {30 ForEach(Tab.allCases) { tab in31 Text(tab.rawValue).tag(tab)32 }33 }34 .labelsHidden()35 .pickerStyle(.segmented)36 .frame(width: 280)37 }38 .padding(ZyquoMetrics.contentInset)3940 ZyquoHairline()4142 switch tab {43 case .provider: ProviderKeysTab()44 case .local: LocalKeysTab()45 }46 }47 }48}4950// MARK: - Provider keys5152private struct ProviderKeysTab: View {53 @EnvironmentObject private var vault: KeyVaultStore54 @EnvironmentObject private var catalog: ModelCatalog5556 var body: some View {57 ScrollView {58 VStack(spacing: 0) {59 ForEach(ProviderID.builtIn) { provider in60 ProviderKeyRow(provider: provider)61 ZyquoHairline()62 .padding(.leading, ZyquoMetrics.contentInset)63 }64 }65 }66 }67}6869private struct ProviderKeyRow: View {70 let provider: ProviderID71 @EnvironmentObject private var vault: KeyVaultStore72 @EnvironmentObject private var catalog: ModelCatalog73 @State private var draft = ""74 @State private var editing = false7576 var body: some View {77 HStack(spacing: ZyquoSpacing.md) {78 HStack(spacing: ZyquoSpacing.xs) {79 Circle()80 .fill(ZyquoColor.providerHue(provider))81 .frame(width: 8, height: 8)82 Text(provider.displayName)83 .font(ZyquoFont.bodyEmphasis())84 .foregroundStyle(ZyquoColor.textPrimary)85 }86 .frame(width: 150, alignment: .leading)8788 if editing || !vault.hasKey(for: provider) {89 SecureField("Paste API key…", text: $draft)90 .textFieldStyle(.roundedBorder)91 .font(ZyquoFont.mono(size: 12))92 .frame(maxWidth: 320)93 Button("Save") {94 vault.setKey(draft, for: provider)95 draft = ""96 editing = false97 }98 .disabled(draft.trimmingCharacters(in: .whitespaces).isEmpty)99 if editing {100 Button("Cancel") {101 draft = ""102 editing = false103 }104 }105 } else {106 Text(vault.redactedKeys[provider] ?? "••••")107 .font(ZyquoFont.mono(size: 12))108 .foregroundStyle(ZyquoColor.textSecondary)109 .frame(maxWidth: 320, alignment: .leading)110 Button("Replace") { editing = true }111 Button("Remove") { vault.deleteKey(for: provider) }112 Button("Test") {113 Task { await vault.testKey(for: provider, catalog: catalog) }114 }115 }116117 Spacer()118 statusView119 }120 .padding(.horizontal, ZyquoMetrics.contentInset)121 .padding(.vertical, ZyquoSpacing.xs)122 }123124 @ViewBuilder125 private var statusView: some View {126 switch vault.statuses[provider] ?? .unset {127 case .unset:128 Text("No key")129 .font(ZyquoFont.caption)130 .foregroundStyle(ZyquoColor.textTertiary)131 case .saved:132 statusDot(ZyquoColor.textTertiary, "Saved")133 case .testing:134 ProgressView()135 .controlSize(.small)136 case .verified(let latency):137 statusDot(ZyquoColor.success, String(format: "OK · %.0f ms", latency * 1000))138 case .failed(let message):139 statusDot(ZyquoColor.danger, message)140 .help(message)141 }142 }143144 private func statusDot(_ color: Color, _ text: String) -> some View {145 HStack(spacing: 5) {146 Circle().fill(color).frame(width: 7, height: 7)147 Text(text)148 .font(ZyquoFont.caption)149 .foregroundStyle(ZyquoColor.textSecondary)150 .lineLimit(1)151 .frame(maxWidth: 220, alignment: .trailing)152 }153 }154}155156// MARK: - Local keys157158private struct LocalKeysTab: View {159 @EnvironmentObject private var localKeys: LocalKeysStore160 @State private var newName = ""161 @State private var revealedToken: String?162163 var body: some View {164 ScrollView {165 VStack(alignment: .leading, spacing: ZyquoSpacing.md) {166 // Create167 HStack(spacing: ZyquoSpacing.xs) {168 TextField("Key name (e.g. \"CLI\", \"VS Code\")", text: $newName)169 .textFieldStyle(.roundedBorder)170 .frame(maxWidth: 280)171 Button("Create key") {172 revealedToken = localKeys.create(name: newName)173 newName = ""174 }175 }176177 if let token = revealedToken {178 VStack(alignment: .leading, spacing: ZyquoSpacing.xxs) {179 Label("Copy this token now — it is shown only once.", systemImage: "exclamationmark.triangle")180 .font(ZyquoFont.caption)181 .foregroundStyle(ZyquoColor.warning)182 HStack {183 CopyField(value: token, size: 12.5)184 Button("Done") { revealedToken = nil }185 }186 }187 .padding(ZyquoSpacing.sm)188 .background(189 RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous)190 .fill(ZyquoColor.accentSubtle)191 )192 }193194 if localKeys.keys.isEmpty {195 Text("No local keys. Without keys, the router is open on localhost; LAN mode requires at least one enabled key.")196 .font(ZyquoFont.body())197 .foregroundStyle(ZyquoColor.textSecondary)198 } else {199 VStack(spacing: 0) {200 ForEach(localKeys.keys) { key in201 LocalKeyRow(record: key)202 ZyquoHairline()203 }204 }205 .zyquoCard()206 }207 }208 .padding(ZyquoMetrics.contentInset)209 }210 }211}212213private struct LocalKeyRow: View {214 let record: APIKeyRecord215 @EnvironmentObject private var localKeys: LocalKeysStore216217 var body: some View {218 HStack(spacing: ZyquoSpacing.md) {219 VStack(alignment: .leading, spacing: 2) {220 Text(record.name)221 .font(ZyquoFont.bodyEmphasis())222 .foregroundStyle(ZyquoColor.textPrimary)223 Text("\(record.tokenPrefix)… · created \(record.createdAt.formatted(date: .abbreviated, time: .omitted))")224 .font(ZyquoFont.mono(size: 11))225 .foregroundStyle(ZyquoColor.textTertiary)226 }227 Spacer()228 Toggle("", isOn: Binding(229 get: { record.enabled },230 set: { localKeys.setEnabled($0, for: record.id) }231 ))232 .toggleStyle(.switch)233 .controlSize(.small)234 .labelsHidden()235 Button("Revoke", role: .destructive) {236 localKeys.revoke(record.id)237 }238 }239 .padding(ZyquoSpacing.sm)240 }241}242