spb/zyquo-local Public MIT
Native macOS AI chat that runs LLMs 100% locally on Apple Silicon with MLX — no cloud, no API keys.
Swift 97.2%
Shell 1.8%
Makefile 1%
1//2// OnboardingHeroView.swift3// Zyquo Local4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//89import SwiftUI1011/// First-run hero: no model downloaded yet. Recommends starter models sized12/// for THIS Mac with one-click download.13struct OnboardingHeroView: View {14 @Environment(AppModel.self) private var app15 @State private var appeared = false1617 var body: some View {18 VStack(spacing: ZyquoTheme.Spacing.xl) {19 Spacer()2021 VStack(spacing: ZyquoTheme.Spacing.m) {22 ZyquoGlyph(size: 56)23 .padding(ZyquoTheme.Spacing.l)24 .background(ZyquoTheme.accentSubtle, in: RoundedRectangle(cornerRadius: 22))25 Text("Download your first model")26 .font(ZyquoTheme.title)27 .foregroundStyle(ZyquoTheme.textPrimary)28 Text("Zyquo Local runs language models entirely on this Mac.\nNothing ever leaves your machine — pick a starter model sized for your \(Int(MemoryAdvisor.physicalMemoryBytes / 1_073_741_824)) GB of memory.")29 .font(ZyquoTheme.body)30 .foregroundStyle(ZyquoTheme.textSecondary)31 .multilineTextAlignment(.center)32 .lineSpacing(ZyquoTheme.lineSpacing(fontSize: 13.5) * 0.5)33 }3435 HStack(spacing: ZyquoTheme.Spacing.m) {36 ForEach(ModelCatalog.starterPicks()) { model in37 StarterCard(model: model)38 }39 }40 .padding(.horizontal, ZyquoTheme.Spacing.xl)4142 Button {43 app.route = .library44 } label: {45 Text("Browse all models in the Library")46 .font(ZyquoTheme.body)47 .foregroundStyle(ZyquoTheme.accent)48 }49 .buttonStyle(.plain)5051 Spacer()52 }53 .frame(maxWidth: .infinity, maxHeight: .infinity)54 .background(ZyquoTheme.background)55 .opacity(appeared ? 1 : 0)56 .offset(y: appeared ? 0 : 8)57 .onAppear {58 withAnimation(.easeOut(duration: 0.25)) { appeared = true }59 }60 }61}6263/// One recommended starter model card.64struct StarterCard: View {65 let model: CatalogModel66 @Environment(AppModel.self) private var app67 @State private var hovering = false6869 private var download: DownloadTask? {70 app.downloads.task(for: model.repoID)71 }7273 var body: some View {74 VStack(alignment: .leading, spacing: ZyquoTheme.Spacing.xs) {75 HStack {76 Text(shortModelName(model.repoID))77 .font(ZyquoTheme.bodyEmphasis)78 .foregroundStyle(ZyquoTheme.textPrimary)79 .lineLimit(1)80 Spacer()81 VerdictBadge(verdict: model.verdict)82 }83 HStack(spacing: ZyquoTheme.Spacing.xxs) {84 InfoBadge(text: model.params)85 InfoBadge(text: model.quant)86 InfoBadge(text: String(format: "%.1f GB", model.sizeGB))87 }88 Text(model.blurb)89 .font(ZyquoTheme.caption)90 .foregroundStyle(ZyquoTheme.textSecondary)91 .lineLimit(2, reservesSpace: true)9293 if let task = download, task.state == .downloading || task.state == .verifying {94 VStack(alignment: .leading, spacing: 3) {95 ProgressView(value: task.fractionCompleted)96 .tint(ZyquoTheme.accent)97 HStack {98 Text("\(Int(task.fractionCompleted * 100))%")99 Spacer()100 if let speed = app.downloads.speeds[model.repoID] {101 Text(formatSpeed(speed))102 }103 }104 .font(ZyquoTheme.caption.monospacedDigit())105 .foregroundStyle(ZyquoTheme.textTertiary)106 }107 } else {108 Button {109 Task { await app.downloads.download(repoID: model.repoID) }110 } label: {111 Text("Download")112 .font(ZyquoTheme.bodyEmphasis)113 .foregroundStyle(.white)114 .frame(maxWidth: .infinity)115 .padding(.vertical, 6)116 .background(ZyquoTheme.accent, in: RoundedRectangle(cornerRadius: ZyquoTheme.Radius.s))117 }118 .buttonStyle(PressableButtonStyle())119 }120 }121 .padding(ZyquoTheme.Spacing.m)122 .frame(width: 190)123 .background(ZyquoTheme.surface, in: RoundedRectangle(cornerRadius: ZyquoTheme.Radius.m))124 .overlay(125 RoundedRectangle(cornerRadius: ZyquoTheme.Radius.m)126 .stroke(hovering ? ZyquoTheme.accent.opacity(0.4) : ZyquoTheme.border, lineWidth: hovering ? 1 : ZyquoTheme.hairline)127 )128 .floatingShadow()129 .onHover { inside in130 withAnimation(.easeOut(duration: 0.08)) { hovering = inside }131 }132 }133}134