SPB Git

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%
5.0 KB · 134 lines swift
Raw Blame History
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