SPB Git

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%
4.3 KB · 139 lines swift
Raw Blame History
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