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// WorkbenchView.swift3// Zyquo MLX4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//89import SwiftUI1011/// The main workbench: 240pt translucent left navigator + contextual header12/// + section content (charter §4.2).13struct WorkbenchView: View {14 @State private var model = AppModel()1516 var body: some View {17 HStack(spacing: 0) {18 SidebarView(model: model)19 .frame(width: ZyquoTheme.sidebarWidth)2021 Rectangle()22 .fill(ZyquoTheme.border)23 .frame(width: ZyquoTheme.hairline)2425 VStack(spacing: 0) {26 HeaderBar(model: model)27 .frame(height: ZyquoTheme.headerHeight)2829 Rectangle()30 .fill(ZyquoTheme.border)31 .frame(height: ZyquoTheme.hairline)3233 sectionContent34 .frame(maxWidth: .infinity, maxHeight: .infinity)35 }36 .background(ZyquoTheme.background)37 }38 .frame(39 minWidth: ZyquoTheme.minWindowSize.width,40 minHeight: ZyquoTheme.minWindowSize.height)41 .task { await model.refresh() }42 .onReceive(NotificationCenter.default.publisher(for: .zyquoNavigate)) { note in43 if let section = note.object as? WorkbenchSection {44 model.selectedSection = section45 }46 }47 }4849 @ViewBuilder50 private var sectionContent: some View {51 switch model.selectedSection {52 case .models: ModelsView(model: model)53 case .datasets: DatasetsView(model: model)54 case .train: TrainView(model: model)55 case .convert: ConvertView(model: model)56 case .playground: PlaygroundView(model: model)57 case .evaluate: EvaluateView(model: model)58 }59 }60}6162// MARK: - Sidebar6364struct SidebarView: View {65 @Bindable var model: AppModel6667 var body: some View {68 VStack(alignment: .leading, spacing: 0) {69 // Wordmark70 HStack(spacing: ZyquoTheme.spacing8) {71 Image(systemName: "z.square.fill")72 .font(.system(size: 22))73 .foregroundStyle(ZyquoTheme.accent)74 Text("Zyquo MLX")75 .font(ZyquoTheme.headlineFont)76 .foregroundStyle(ZyquoTheme.textPrimary)77 }78 .padding(.horizontal, ZyquoTheme.spacing16)79 .padding(.top, ZyquoTheme.spacing20)80 .padding(.bottom, ZyquoTheme.spacing24)8182 ForEach(WorkbenchSection.allCases) { section in83 SidebarRow(84 section: section,85 isSelected: model.selectedSection == section,86 badge: badge(for: section)87 ) {88 withAnimation(ZyquoTheme.quickAnimation) {89 model.selectedSection = section90 }91 }92 }9394 Spacer()9596 SidebarFooter(model: model)97 }98 .background(.ultraThinMaterial)99 }100101 private func badge(for section: WorkbenchSection) -> String? {102 switch section {103 case .models: model.models.isEmpty ? nil : "\(model.models.count)"104 case .datasets: model.datasets.isEmpty ? nil : "\(model.datasets.count)"105 case .train: model.runs.isEmpty ? nil : "\(model.runs.count)"106 default: nil107 }108 }109}110111private struct SidebarRow: View {112 let section: WorkbenchSection113 let isSelected: Bool114 let badge: String?115 let action: () -> Void116117 @State private var isHovering = false118119 var body: some View {120 Button(action: action) {121 HStack(spacing: ZyquoTheme.spacing12) {122 Image(systemName: section.icon)123 .font(.system(size: 14, weight: .medium))124 .foregroundStyle(isSelected ? ZyquoTheme.accent : ZyquoTheme.textSecondary)125 .frame(width: 20)126 Text(section.title)127 .font(ZyquoTheme.bodyFont.weight(isSelected ? .semibold : .regular))128 .foregroundStyle(isSelected ? ZyquoTheme.textPrimary : ZyquoTheme.textSecondary)129 Spacer()130 if let badge {131 Text(badge)132 .font(ZyquoTheme.captionFont)133 .foregroundStyle(ZyquoTheme.textTertiary)134 }135 }136 .padding(.horizontal, ZyquoTheme.spacing12)137 .padding(.vertical, ZyquoTheme.spacing8)138 .background(139 RoundedRectangle(cornerRadius: ZyquoTheme.radiusSmall)140 .fill(141 isSelected142 ? ZyquoTheme.accentSubtle143 : isHovering ? ZyquoTheme.surfaceSecondary : .clear)144 )145 .contentShape(Rectangle())146 }147 .buttonStyle(.plain)148 .padding(.horizontal, ZyquoTheme.spacing8)149 .onHover { isHovering = $0 }150 }151}152153private struct SidebarFooter: View {154 @Bindable var model: AppModel155 @Environment(\.openSettings) private var openSettings156157 var body: some View {158 VStack(spacing: ZyquoTheme.spacing8) {159 Rectangle()160 .fill(ZyquoTheme.border)161 .frame(height: ZyquoTheme.hairline)162163 HStack(spacing: ZyquoTheme.spacing8) {164 Button {165 openSettings()166 } label: {167 Image(systemName: "gearshape")168 .font(.system(size: 13))169 .foregroundStyle(ZyquoTheme.textSecondary)170 }171 .buttonStyle(.plain)172 .help("Settings")173174 if model.activeJobCount > 0 {175 HStack(spacing: ZyquoTheme.spacing4) {176 ProgressView()177 .controlSize(.small)178 Text("\(model.activeJobCount)")179 .font(ZyquoTheme.captionFont)180 .foregroundStyle(ZyquoTheme.textSecondary)181 }182 .help("Active jobs")183 }184185 Spacer()186187 HStack(spacing: ZyquoTheme.spacing4) {188 Circle()189 .fill(memoryColor)190 .frame(width: 7, height: 7)191 Text(memoryLabel)192 .font(ZyquoTheme.monoSmallFont)193 .foregroundStyle(ZyquoTheme.textTertiary)194 }195 .help("GPU memory in use / unified memory")196 }197 .padding(.horizontal, ZyquoTheme.spacing16)198 .padding(.bottom, ZyquoTheme.spacing12)199 }200 }201202 private var memoryLabel: String {203 let used = Double(model.memoryUsedBytes) / 1_073_741_824204 let total = Double(model.memoryTotalBytes) / 1_073_741_824205 return String(format: "%.1f / %.0f GB", used, total)206 }207208 private var memoryColor: Color {209 let fraction = Double(model.memoryUsedBytes) / Double(max(1, model.memoryTotalBytes))210 if fraction > 0.75 { return ZyquoTheme.danger }211 if fraction > 0.5 { return ZyquoTheme.warning }212 return ZyquoTheme.success213 }214}215216// MARK: - Header217218struct HeaderBar: View {219 @Bindable var model: AppModel220221 var body: some View {222 HStack(spacing: ZyquoTheme.spacing12) {223 Text(model.selectedSection.title)224 .font(ZyquoTheme.titleFont)225 .foregroundStyle(ZyquoTheme.textPrimary)226227 Spacer()228229 if model.activeJobCount > 0 {230 StatusPill(231 text: "\(model.activeJobCount) active job\(model.activeJobCount > 1 ? "s" : "")",232 color: ZyquoTheme.accent)233 }234235 Button {236 Task { await model.refresh() }237 } label: {238 Image(systemName: "arrow.clockwise")239 .font(.system(size: 12, weight: .medium))240 .foregroundStyle(ZyquoTheme.textSecondary)241 }242 .buttonStyle(.plain)243 .help("Refresh library")244 }245 .padding(.horizontal, ZyquoTheme.spacing20)246 .background(ZyquoTheme.background)247 }248}249