spb/zyquo-mlx Public MIT
The local MLX foundry for your Mac — run, fine-tune, quantize, and ship models. Nothing leaves your machine.
Swift 93.4%
Python 3.8%
Makefile 2.2%
Shell 0.5%
1//2// DiscoverView.swift3// Zyquo MLX4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//89import SwiftUI1011/// Models › Discover: Featured catalog + live mlx-community search with12/// resumable downloads (charter Phase 6).13struct DiscoverView: View {14 @Bindable var model: AppModel15 @State private var query = ""16 @State private var results: [HubModel] = []17 @State private var isSearching = false18 @State private var searchError: String?19 @State private var downloads: [String: DownloadProgress] = [:]20 @State private var downloadErrors: [String: String] = [:]2122 private var installedIDs: Set<String> {23 Set(model.models.compactMap(\.repoID))24 }2526 var body: some View {27 VStack(spacing: 0) {28 searchField29 .padding(.horizontal, ZyquoTheme.spacing20)30 .padding(.bottom, ZyquoTheme.spacing12)3132 ScrollView {33 LazyVStack(alignment: .leading, spacing: ZyquoTheme.spacing8) {34 if let searchError {35 Text(searchError)36 .font(ZyquoTheme.captionFont)37 .foregroundStyle(ZyquoTheme.danger)38 }39 if query.isEmpty {40 Text("Featured — live-verified for this Mac")41 .font(ZyquoTheme.headlineFont)42 .foregroundStyle(ZyquoTheme.textPrimary)43 .padding(.bottom, ZyquoTheme.spacing4)44 ForEach(Catalog.featured) { entry in45 DiscoverRow(46 repoID: entry.id,47 title: entry.name,48 subtitle: "\(entry.params) · \(entry.quant) · \(ByteCountFormatter.string(fromByteCount: entry.weightBytes, countStyle: .file)) — \(entry.blurb)",49 type: entry.type,50 verdict: entry.verdict,51 installed: installedIDs.contains(entry.id),52 progress: downloads[entry.id],53 error: downloadErrors[entry.id],54 download: { download(repo: entry.id) },55 cancel: { Task { await DownloadManager.shared.cancel(repo: entry.id) } })56 }57 } else if isSearching {58 HStack {59 ProgressView().controlSize(.small)60 Text("Searching mlx models…")61 .font(ZyquoTheme.captionFont)62 .foregroundStyle(ZyquoTheme.textSecondary)63 }64 .frame(maxWidth: .infinity)65 .padding(.top, ZyquoTheme.spacing32)66 } else {67 ForEach(results) { result in68 DiscoverRow(69 repoID: result.id,70 title: result.id,71 subtitle: "\(result.downloads.formatted()) downloads · \(result.likes) likes"72 + (result.gated ? " · gated" : ""),73 type: result.modelType,74 verdict: nil,75 installed: installedIDs.contains(result.id),76 progress: downloads[result.id],77 error: downloadErrors[result.id],78 download: { download(repo: result.id) },79 cancel: { Task { await DownloadManager.shared.cancel(repo: result.id) } })80 }81 if results.isEmpty {82 Text("No MLX models match “\(query)”.")83 .font(ZyquoTheme.bodyFont)84 .foregroundStyle(ZyquoTheme.textSecondary)85 .frame(maxWidth: .infinity)86 .padding(.top, ZyquoTheme.spacing32)87 }88 }89 }90 .padding(.horizontal, ZyquoTheme.spacing20)91 .padding(.bottom, ZyquoTheme.spacing20)92 }93 }94 .task(id: query) {95 guard !query.isEmpty else { return }96 isSearching = true97 searchError = nil98 try? await Task.sleep(for: .milliseconds(350)) // debounce99 guard !Task.isCancelled else { return }100 do {101 results = try await HubService.shared.search(query: query)102 } catch {103 searchError = error.localizedDescription104 }105 isSearching = false106 }107 }108109 private var searchField: some View {110 HStack(spacing: ZyquoTheme.spacing8) {111 Image(systemName: "magnifyingglass")112 .foregroundStyle(ZyquoTheme.textTertiary)113 TextField("Search mlx-community models…", text: $query)114 .textFieldStyle(.plain)115 .font(ZyquoTheme.bodyFont)116 }117 .padding(.horizontal, ZyquoTheme.spacing12)118 .padding(.vertical, ZyquoTheme.spacing8)119 .background(ZyquoTheme.surfaceSecondary)120 .clipShape(RoundedRectangle(cornerRadius: ZyquoTheme.radiusSmall))121 }122123 private func download(repo: String) {124 downloadErrors[repo] = nil125 Task {126 let events = await DownloadManager.shared.download(repo: repo)127 for await event in events {128 switch event {129 case .progress(let progress):130 downloads[repo] = progress131 case .finished:132 downloads[repo] = nil133 await model.refresh()134 case .failed(let message):135 downloads[repo] = nil136 downloadErrors[repo] = message137 }138 }139 }140 }141}142143private struct DiscoverRow: View {144 let repoID: String145 let title: String146 let subtitle: String147 let type: ModelType148 let verdict: MemoryVerdict?149 let installed: Bool150 let progress: DownloadProgress?151 let error: String?152 let download: () -> Void153 let cancel: () -> Void154155 var body: some View {156 HStack(spacing: ZyquoTheme.spacing12) {157 VStack(alignment: .leading, spacing: ZyquoTheme.spacing2) {158 Text(title)159 .font(ZyquoTheme.bodyFont.weight(.medium))160 .foregroundStyle(ZyquoTheme.textPrimary)161 .lineLimit(1)162 Text(error ?? subtitle)163 .font(ZyquoTheme.captionFont)164 .foregroundStyle(error != nil ? ZyquoTheme.danger : ZyquoTheme.textSecondary)165 .lineLimit(2)166 }167168 Spacer()169170 TypeBadge(type: type)171 if let verdict {172 VerdictBadge(verdict: verdict)173 }174175 if installed {176 StatusPill(text: "Installed", color: ZyquoTheme.success)177 } else if let progress {178 HStack(spacing: ZyquoTheme.spacing8) {179 ProgressView(value: progress.fraction)180 .frame(width: 90)181 Text("\(Int(progress.fraction * 100))%")182 .font(ZyquoTheme.monoSmallFont)183 .foregroundStyle(ZyquoTheme.textSecondary)184 Button(action: cancel) {185 Image(systemName: "pause.circle")186 .foregroundStyle(ZyquoTheme.textSecondary)187 }188 .buttonStyle(.plain)189 .help("Pause (resumes from the same byte)")190 }191 } else {192 Button(action: download) {193 Image(systemName: "arrow.down.circle.fill")194 .font(.system(size: 20))195 .foregroundStyle(ZyquoTheme.accent)196 }197 .buttonStyle(.plain)198 .help("Download")199 }200 }201 .padding(ZyquoTheme.spacing12)202 .zyquoCard()203 }204}205