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.
Swift 95.7%
Shell 4.3%
1// Author: Simon-Pierre Boucher — contact@spboucher.ai2//3// Navigation shell: runs in the sidebar, live dashboard in the detail pane.4import SwiftUI56enum SidebarItem: Hashable {7 case compare8 case datasets9 case run(UUID)10}1112struct ContentView: View {13 @Environment(AppModel.self) private var app14 @State private var selection: SidebarItem?15 @State private var showNewRun = false1617 var body: some View {18 NavigationSplitView {19 List(selection: $selection) {20 Section("Studio") {21 Label("Comparer", systemImage: "chart.xyaxis.line")22 .tag(SidebarItem.compare)23 Label("Datasets", systemImage: "cylinder.split.1x2")24 .tag(SidebarItem.datasets)25 }26 Section("Runs") {27 ForEach(app.store.runs.sorted { $0.createdAt > $1.createdAt }) { run in28 RunRow(run: run).tag(SidebarItem.run(run.id))29 }30 }31 }32 .navigationSplitViewColumnWidth(min: 220, ideal: 260)33 .toolbar {34 ToolbarItem(placement: .primaryAction) {35 Button {36 showNewRun = true37 } label: {38 Label("Nouveau run", systemImage: "plus")39 }40 .disabled(app.forgeError != nil)41 }42 }43 } detail: {44 switch selection {45 case .compare:46 CompareView()47 case .datasets:48 DatasetsView()49 case .run(let id):50 if let run = app.store.runs.first(where: { $0.id == id }) {51 RunDetailView(run: run)52 } else {53 EmptyStateView()54 }55 case nil:56 EmptyStateView()57 }58 }59 .sheet(isPresented: $showNewRun) {60 NewRunSheet()61 }62 }63}6465struct RunRow: View {66 let run: Run6768 var body: some View {69 VStack(alignment: .leading, spacing: 2) {70 HStack {71 Text(run.name).font(.headline)72 Spacer()73 StateBadge(state: run.state)74 }75 HStack(spacing: 8) {76 Text("\(run.config.model.paramCount.formatted(.number.notation(.compactName))) params")77 if let loss = run.lastTrainLoss {78 Text(String(format: "loss %.3f", loss))79 }80 if run.state.isActive {81 ProgressView(value: run.progress).frame(width: 60)82 }83 }84 .font(.caption)85 .foregroundStyle(.secondary)86 }87 .padding(.vertical, 2)88 }89}9091struct StateBadge: View {92 let state: RunState9394 var color: Color {95 switch state {96 case .running, .launching: return .green97 case .finished: return .blue98 case .failed: return .red99 case .stopped: return .orange100 case .queued, .finishing: return .gray101 }102 }103104 var body: some View {105 Text(state.rawValue)106 .font(.caption2.weight(.semibold))107 .padding(.horizontal, 6)108 .padding(.vertical, 2)109 .background(color.opacity(0.18), in: Capsule())110 .foregroundStyle(color)111 }112}113114struct EmptyStateView: View {115 @Environment(AppModel.self) private var app116117 var body: some View {118 VStack(spacing: 12) {119 Image(systemName: "flame")120 .font(.system(size: 48))121 .foregroundStyle(.orange)122 Text("Forge Studio").font(.largeTitle.weight(.semibold))123 if let device = app.forgeDevice {124 Label(device, systemImage: "cpu")125 .foregroundStyle(.secondary)126 }127 if let error = app.forgeError {128 Label(error, systemImage: "exclamationmark.triangle")129 .foregroundStyle(.red)130 SettingsLink { Text("Ouvrir les Réglages…") }131 } else {132 Text("Sélectionnez un run, ou créez-en un nouveau (+).")133 .foregroundStyle(.secondary)134 }135 }136 .frame(maxWidth: .infinity, maxHeight: .infinity)137 }138}139