spb/forge-studio
Public
The Instruments of LLM training — a native macOS cockpit for Forge. Train language models from scratch on Apple Silicon without a terminal.
1// Author: Simon-Pierre Boucher — contact@spboucher.ai2//3// Navigation shell: runs in the sidebar, live dashboard in the detail pane.4import SwiftUI56struct ContentView: View {7 @Environment(AppModel.self) private var app8 @State private var selection: UUID?9 @State private var showNewRun = false1011 var body: some View {12 NavigationSplitView {13 List(selection: $selection) {14 Section("Runs") {15 ForEach(app.store.runs.sorted { $0.createdAt > $1.createdAt }) { run in16 RunRow(run: run).tag(run.id)17 }18 }19 }20 .navigationSplitViewColumnWidth(min: 220, ideal: 260)21 .toolbar {22 ToolbarItem(placement: .primaryAction) {23 Button {24 showNewRun = true25 } label: {26 Label("Nouveau run", systemImage: "plus")27 }28 .disabled(app.forgeError != nil)29 }30 }31 } detail: {32 if let id = selection,33 let run = app.store.runs.first(where: { $0.id == id }) {34 RunDetailView(run: run)35 } else {36 EmptyStateView()37 }38 }39 .sheet(isPresented: $showNewRun) {40 NewRunSheet()41 }42 }43}4445struct RunRow: View {46 let run: Run4748 var body: some View {49 VStack(alignment: .leading, spacing: 2) {50 HStack {51 Text(run.name).font(.headline)52 Spacer()53 StateBadge(state: run.state)54 }55 HStack(spacing: 8) {56 Text("\(run.config.model.paramCount.formatted(.number.notation(.compactName))) params")57 if let loss = run.lastTrainLoss {58 Text(String(format: "loss %.3f", loss))59 }60 if run.state.isActive {61 ProgressView(value: run.progress).frame(width: 60)62 }63 }64 .font(.caption)65 .foregroundStyle(.secondary)66 }67 .padding(.vertical, 2)68 }69}7071struct StateBadge: View {72 let state: RunState7374 var color: Color {75 switch state {76 case .running, .launching: return .green77 case .finished: return .blue78 case .failed: return .red79 case .stopped: return .orange80 case .queued, .finishing: return .gray81 }82 }8384 var body: some View {85 Text(state.rawValue)86 .font(.caption2.weight(.semibold))87 .padding(.horizontal, 6)88 .padding(.vertical, 2)89 .background(color.opacity(0.18), in: Capsule())90 .foregroundStyle(color)91 }92}9394struct EmptyStateView: View {95 @Environment(AppModel.self) private var app9697 var body: some View {98 VStack(spacing: 12) {99 Image(systemName: "flame")100 .font(.system(size: 48))101 .foregroundStyle(.orange)102 Text("Forge Studio").font(.largeTitle.weight(.semibold))103 if let device = app.forgeDevice {104 Label(device, systemImage: "cpu")105 .foregroundStyle(.secondary)106 }107 if let error = app.forgeError {108 Label(error, systemImage: "exclamationmark.triangle")109 .foregroundStyle(.red)110 SettingsLink { Text("Ouvrir les Réglages…") }111 } else {112 Text("Sélectionnez un run, ou créez-en un nouveau (+).")113 .foregroundStyle(.secondary)114 }115 }116 .frame(maxWidth: .infinity, maxHeight: .infinity)117 }118}119