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// DocsView.swift3// Zyquo Router4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// In-app rendering of docs/API.md — the same source of truth as the served9// behavior, rendered as a polished reference: hero header, real tables,10// code blocks with a header bar and one-click copy, generous rounding.11// The file is bundled by the Makefile; a repo-relative fallback covers12// `swift run` during development.13//1415import SwiftUI1617struct DocsView: View {18 @State private var markdown: String?1920 var body: some View {21 Group {22 if let markdown {23 ScrollView {24 VStack(alignment: .leading, spacing: ZyquoSpacing.md) {25 DocsHero()26 MarkdownText(markdown: markdown)27 }28 .padding(ZyquoSpacing.xl)29 .frame(maxWidth: 820, alignment: .leading)30 }31 .frame(maxWidth: .infinity)32 } else {33 EmptyState(34 systemImage: "book",35 title: "API reference unavailable",36 message: "docs/API.md was not found in the app bundle."37 )38 }39 }40 .onAppear(perform: load)41 }4243 private func load() {44 if let bundled = Bundle.main.url(forResource: "API", withExtension: "md"),45 let text = try? String(contentsOf: bundled, encoding: .utf8) {46 markdown = text47 return48 }49 // Development fallback: repo checkout next to the executable's cwd.50 let repoDocs = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)51 .appendingPathComponent("docs/API.md")52 markdown = try? String(contentsOf: repoDocs, encoding: .utf8)53 }54}5556/// Hero header card at the top of the reference.57private struct DocsHero: View {58 @EnvironmentObject private var server: ServerController5960 var body: some View {61 VStack(alignment: .leading, spacing: ZyquoSpacing.xs) {62 HStack(spacing: ZyquoSpacing.xs) {63 RouterZGlyph(size: 22)64 Text("API Reference")65 .font(.system(size: 24, weight: .bold))66 .foregroundStyle(ZyquoColor.textPrimary)67 }68 Text("One OpenAI-compatible endpoint for every provider. Point any SDK at your local base URL — this reference is generated from the same source of truth the server implements.")69 .font(ZyquoFont.body(size: 13.5))70 .foregroundStyle(ZyquoColor.textSecondary)71 .lineSpacing(3)72 HStack(spacing: ZyquoSpacing.xs) {73 Text(server.isRunning ? server.endpointURL : "http://localhost:\(server.port)/v1")74 .font(ZyquoFont.mono(size: 12.5, weight: .medium))75 .foregroundStyle(ZyquoColor.accent)76 CopyButton(value: server.isRunning ? server.endpointURL : "http://localhost:\(server.port)/v1")77 if !server.isRunning {78 CapabilityBadge(label: "SERVER STOPPED", tint: ZyquoColor.warning)79 }80 }81 .padding(.top, 2)82 }83 .padding(ZyquoSpacing.lg)84 .frame(maxWidth: .infinity, alignment: .leading)85 .background(86 RoundedRectangle(cornerRadius: ZyquoRadius.large, style: .continuous)87 .fill(88 LinearGradient(89 colors: [ZyquoColor.accentSubtle, ZyquoColor.surface],90 startPoint: .topLeading, endPoint: .bottomTrailing91 )92 )93 .overlay(94 RoundedRectangle(cornerRadius: ZyquoRadius.large, style: .continuous)95 .strokeBorder(ZyquoColor.accent.opacity(0.18), lineWidth: ZyquoMetrics.hairline)96 )97 )98 }99}100101// MARK: - Markdown renderer102103private struct MarkdownText: View {104 let markdown: String105106 var body: some View {107 VStack(alignment: .leading, spacing: ZyquoSpacing.sm) {108 ForEach(Array(blocks.enumerated()), id: \.offset) { _, block in109 render(block)110 }111 }112 }113114 private enum Block {115 case heading(level: Int, text: String)116 case code(String)117 case table(header: [String], rows: [[String]])118 case paragraph(String)119 case bullet(String)120 case rule121 }122123 private var blocks: [Block] {124 var result: [Block] = []125 var paragraph: [String] = []126 var codeBlock: [String]?127 var tableLines: [String] = []128129 func flushParagraph() {130 if !paragraph.isEmpty {131 result.append(.paragraph(paragraph.joined(separator: "\n")))132 paragraph = []133 }134 }135 func flushTable() {136 guard tableLines.count >= 2 else { tableLines = []; return }137 func cells(_ line: String) -> [String] {138 line.trimmingCharacters(in: CharacterSet(charactersIn: "| "))139 .components(separatedBy: " | ")140 .map { $0.trimmingCharacters(in: .whitespaces) }141 }142 let header = cells(tableLines[0])143 let rows = tableLines.dropFirst(2).map(cells)144 result.append(.table(header: header, rows: Array(rows)))145 tableLines = []146 }147148 for line in markdown.components(separatedBy: "\n") {149 if var code = codeBlock {150 if line.hasPrefix("```") {151 result.append(.code(code.joined(separator: "\n")))152 codeBlock = nil153 } else {154 code.append(line)155 codeBlock = code156 }157 continue158 }159 if line.hasPrefix("```") {160 flushParagraph(); flushTable()161 codeBlock = []162 continue163 }164 if line.hasPrefix("|") {165 flushParagraph()166 tableLines.append(line)167 continue168 }169 flushTable()170 if line.hasPrefix("#") {171 flushParagraph()172 let level = line.prefix(while: { $0 == "#" }).count173 result.append(.heading(level: level, text: line.drop(while: { $0 == "#" }).trimmingCharacters(in: .whitespaces)))174 } else if line.hasPrefix("---") {175 flushParagraph()176 result.append(.rule)177 } else if line.hasPrefix("- ") {178 flushParagraph()179 result.append(.bullet(String(line.dropFirst(2))))180 } else if line.trimmingCharacters(in: .whitespaces).isEmpty {181 flushParagraph()182 } else if line.hasPrefix(" "), paragraph.isEmpty,183 case .bullet(let text) = result.last {184 // Hard-wrapped bullet continuation line.185 result[result.count - 1] = .bullet(text + " " + line.trimmingCharacters(in: .whitespaces))186 } else {187 paragraph.append(line)188 }189 }190 flushParagraph(); flushTable()191 return result192 }193194 @ViewBuilder195 private func render(_ block: Block) -> some View {196 switch block {197 case .heading(let level, let text):198 heading(level: level, text: text)199 case .code(let code):200 CodeCard(code: code)201 case .table(let header, let rows):202 TableCard(header: header, rows: rows)203 case .paragraph(let text):204 Text(attributed(text))205 .font(ZyquoFont.body(size: 13.5))206 .foregroundStyle(ZyquoColor.textPrimary)207 .lineSpacing(3.5)208 case .bullet(let text):209 HStack(alignment: .top, spacing: ZyquoSpacing.xs) {210 Circle()211 .fill(ZyquoColor.accent)212 .frame(width: 5, height: 5)213 .padding(.top, 7)214 Text(attributed(text))215 .font(ZyquoFont.body(size: 13.5))216 .foregroundStyle(ZyquoColor.textPrimary)217 .lineSpacing(3)218 }219 .padding(.leading, ZyquoSpacing.xxs)220 case .rule:221 ZyquoHairline()222 .padding(.vertical, ZyquoSpacing.xs)223 }224 }225226 @ViewBuilder227 private func heading(level: Int, text: String) -> some View {228 if level <= 1 {229 EmptyView() // the hero replaces the H1230 } else if level == 2 {231 HStack(spacing: ZyquoSpacing.xs) {232 RoundedRectangle(cornerRadius: 2, style: .continuous)233 .fill(ZyquoColor.accent)234 .frame(width: 4, height: 18)235 Text(attributed(text))236 .font(.system(size: 18, weight: .semibold))237 .foregroundStyle(ZyquoColor.textPrimary)238 }239 .padding(.top, ZyquoSpacing.lg)240 } else {241 Text(attributed(text))242 .font(ZyquoFont.heading)243 .foregroundStyle(ZyquoColor.textPrimary)244 .padding(.top, ZyquoSpacing.xs)245 }246 }247248 private func attributed(_ text: String) -> AttributedString {249 (try? AttributedString(250 markdown: text,251 options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace)252 )) ?? AttributedString(text)253 }254}255256// MARK: - Code card257258private struct CodeCard: View {259 let code: String260261 private var language: String {262 if code.contains("import OpenAI") || code.contains("await client") { return "javascript" }263 if code.contains("from openai") || code.contains("client =") { return "python" }264 if code.hasPrefix("curl") { return "bash" }265 if code.hasPrefix("{") || code.hasPrefix("[") { return "json" }266 return "code"267 }268269 var body: some View {270 VStack(spacing: 0) {271 HStack(spacing: ZyquoSpacing.xs) {272 HStack(spacing: 4) {273 ForEach(0..<3, id: \.self) { _ in274 Circle()275 .fill(ZyquoColor.border)276 .frame(width: 7, height: 7)277 }278 }279 Text(language)280 .font(ZyquoFont.mono(size: 10))281 .foregroundStyle(ZyquoColor.textTertiary)282 Spacer()283 CopyButton(value: code)284 }285 .padding(.horizontal, ZyquoSpacing.sm)286 .padding(.vertical, ZyquoSpacing.xs)287 .background(ZyquoColor.surfaceSecondary)288289 ZyquoHairline()290291 ScrollView(.horizontal, showsIndicators: false) {292 Text(code)293 .font(ZyquoFont.mono(size: 12))294 .foregroundStyle(ZyquoColor.textPrimary)295 .lineSpacing(2.5)296 .textSelection(.enabled)297 .padding(ZyquoSpacing.sm)298 }299 .background(ZyquoColor.surface)300 }301 .clipShape(RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous))302 .overlay(303 RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous)304 .strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline)305 )306 }307}308309// MARK: - Table card310311private struct TableCard: View {312 let header: [String]313 let rows: [[String]]314315 var body: some View {316 VStack(spacing: 0) {317 row(cells: header, isHeader: true)318 ForEach(Array(rows.enumerated()), id: \.offset) { index, cells in319 ZyquoHairline()320 row(cells: cells, isHeader: false)321 .background(index.isMultiple(of: 2) ? ZyquoColor.surface : ZyquoColor.surfaceSecondary.opacity(0.5))322 }323 }324 .clipShape(RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous))325 .overlay(326 RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous)327 .strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline)328 )329 }330331 private func row(cells: [String], isHeader: Bool) -> some View {332 HStack(alignment: .top, spacing: ZyquoSpacing.sm) {333 ForEach(Array(cells.enumerated()), id: \.offset) { index, cell in334 Group {335 if isHeader {336 Text(cell.uppercased())337 .font(.system(size: 9.5, weight: .semibold))338 .foregroundStyle(ZyquoColor.textTertiary)339 .kerning(0.4)340 } else {341 Text(inline(cell))342 .font(ZyquoFont.body(size: 12))343 .foregroundStyle(ZyquoColor.textPrimary)344 .lineSpacing(2)345 }346 }347 .frame(maxWidth: index == 0 ? 190 : .infinity, alignment: .leading)348 }349 }350 .padding(.horizontal, ZyquoSpacing.sm)351 .padding(.vertical, ZyquoSpacing.xs)352 .background(isHeader ? ZyquoColor.surfaceSecondary : .clear)353 }354355 private func inline(_ text: String) -> AttributedString {356 (try? AttributedString(357 markdown: text,358 options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace)359 )) ?? AttributedString(text)360 }361}362