// // WorkbenchView.swift // Zyquo MLX // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // import SwiftUI /// The main workbench: 240pt translucent left navigator + contextual header /// + section content (charter ยง4.2). struct WorkbenchView: View { @State private var model = AppModel() var body: some View { HStack(spacing: 0) { SidebarView(model: model) .frame(width: ZyquoTheme.sidebarWidth) Rectangle() .fill(ZyquoTheme.border) .frame(width: ZyquoTheme.hairline) VStack(spacing: 0) { HeaderBar(model: model) .frame(height: ZyquoTheme.headerHeight) Rectangle() .fill(ZyquoTheme.border) .frame(height: ZyquoTheme.hairline) sectionContent .frame(maxWidth: .infinity, maxHeight: .infinity) } .background(ZyquoTheme.background) } .frame( minWidth: ZyquoTheme.minWindowSize.width, minHeight: ZyquoTheme.minWindowSize.height) .task { await model.refresh() } .onReceive(NotificationCenter.default.publisher(for: .zyquoNavigate)) { note in if let section = note.object as? WorkbenchSection { model.selectedSection = section } } } @ViewBuilder private var sectionContent: some View { switch model.selectedSection { case .models: ModelsView(model: model) case .datasets: DatasetsView(model: model) case .train: TrainView(model: model) case .convert: ConvertView(model: model) case .playground: PlaygroundView(model: model) case .evaluate: EvaluateView(model: model) } } } // MARK: - Sidebar struct SidebarView: View { @Bindable var model: AppModel var body: some View { VStack(alignment: .leading, spacing: 0) { // Wordmark HStack(spacing: ZyquoTheme.spacing8) { Image(systemName: "z.square.fill") .font(.system(size: 22)) .foregroundStyle(ZyquoTheme.accent) Text("Zyquo MLX") .font(ZyquoTheme.headlineFont) .foregroundStyle(ZyquoTheme.textPrimary) } .padding(.horizontal, ZyquoTheme.spacing16) .padding(.top, ZyquoTheme.spacing20) .padding(.bottom, ZyquoTheme.spacing24) ForEach(WorkbenchSection.allCases) { section in SidebarRow( section: section, isSelected: model.selectedSection == section, badge: badge(for: section) ) { withAnimation(ZyquoTheme.quickAnimation) { model.selectedSection = section } } } Spacer() SidebarFooter(model: model) } .background(.ultraThinMaterial) } private func badge(for section: WorkbenchSection) -> String? { switch section { case .models: model.models.isEmpty ? nil : "\(model.models.count)" case .datasets: model.datasets.isEmpty ? nil : "\(model.datasets.count)" case .train: model.runs.isEmpty ? nil : "\(model.runs.count)" default: nil } } } private struct SidebarRow: View { let section: WorkbenchSection let isSelected: Bool let badge: String? let action: () -> Void @State private var isHovering = false var body: some View { Button(action: action) { HStack(spacing: ZyquoTheme.spacing12) { Image(systemName: section.icon) .font(.system(size: 14, weight: .medium)) .foregroundStyle(isSelected ? ZyquoTheme.accent : ZyquoTheme.textSecondary) .frame(width: 20) Text(section.title) .font(ZyquoTheme.bodyFont.weight(isSelected ? .semibold : .regular)) .foregroundStyle(isSelected ? ZyquoTheme.textPrimary : ZyquoTheme.textSecondary) Spacer() if let badge { Text(badge) .font(ZyquoTheme.captionFont) .foregroundStyle(ZyquoTheme.textTertiary) } } .padding(.horizontal, ZyquoTheme.spacing12) .padding(.vertical, ZyquoTheme.spacing8) .background( RoundedRectangle(cornerRadius: ZyquoTheme.radiusSmall) .fill( isSelected ? ZyquoTheme.accentSubtle : isHovering ? ZyquoTheme.surfaceSecondary : .clear) ) .contentShape(Rectangle()) } .buttonStyle(.plain) .padding(.horizontal, ZyquoTheme.spacing8) .onHover { isHovering = $0 } } } private struct SidebarFooter: View { @Bindable var model: AppModel @Environment(\.openSettings) private var openSettings var body: some View { VStack(spacing: ZyquoTheme.spacing8) { Rectangle() .fill(ZyquoTheme.border) .frame(height: ZyquoTheme.hairline) HStack(spacing: ZyquoTheme.spacing8) { Button { openSettings() } label: { Image(systemName: "gearshape") .font(.system(size: 13)) .foregroundStyle(ZyquoTheme.textSecondary) } .buttonStyle(.plain) .help("Settings") if model.activeJobCount > 0 { HStack(spacing: ZyquoTheme.spacing4) { ProgressView() .controlSize(.small) Text("\(model.activeJobCount)") .font(ZyquoTheme.captionFont) .foregroundStyle(ZyquoTheme.textSecondary) } .help("Active jobs") } Spacer() HStack(spacing: ZyquoTheme.spacing4) { Circle() .fill(memoryColor) .frame(width: 7, height: 7) Text(memoryLabel) .font(ZyquoTheme.monoSmallFont) .foregroundStyle(ZyquoTheme.textTertiary) } .help("GPU memory in use / unified memory") } .padding(.horizontal, ZyquoTheme.spacing16) .padding(.bottom, ZyquoTheme.spacing12) } } private var memoryLabel: String { let used = Double(model.memoryUsedBytes) / 1_073_741_824 let total = Double(model.memoryTotalBytes) / 1_073_741_824 return String(format: "%.1f / %.0f GB", used, total) } private var memoryColor: Color { let fraction = Double(model.memoryUsedBytes) / Double(max(1, model.memoryTotalBytes)) if fraction > 0.75 { return ZyquoTheme.danger } if fraction > 0.5 { return ZyquoTheme.warning } return ZyquoTheme.success } } // MARK: - Header struct HeaderBar: View { @Bindable var model: AppModel var body: some View { HStack(spacing: ZyquoTheme.spacing12) { Text(model.selectedSection.title) .font(ZyquoTheme.titleFont) .foregroundStyle(ZyquoTheme.textPrimary) Spacer() if model.activeJobCount > 0 { StatusPill( text: "\(model.activeJobCount) active job\(model.activeJobCount > 1 ? "s" : "")", color: ZyquoTheme.accent) } Button { Task { await model.refresh() } } label: { Image(systemName: "arrow.clockwise") .font(.system(size: 12, weight: .medium)) .foregroundStyle(ZyquoTheme.textSecondary) } .buttonStyle(.plain) .help("Refresh library") } .padding(.horizontal, ZyquoTheme.spacing20) .background(ZyquoTheme.background) } }