// Author: Simon-Pierre Boucher — contact@spboucher.ai // // Navigation shell: runs in the sidebar, live dashboard in the detail pane. import SwiftUI enum SidebarItem: Hashable { case compare case datasets case run(UUID) } struct ContentView: View { @Environment(AppModel.self) private var app @State private var selection: SidebarItem? @State private var showNewRun = false var body: some View { NavigationSplitView { List(selection: $selection) { Section("Studio") { Label("Comparer", systemImage: "chart.xyaxis.line") .tag(SidebarItem.compare) Label("Datasets", systemImage: "cylinder.split.1x2") .tag(SidebarItem.datasets) } Section("Runs") { ForEach(app.store.runs.sorted { $0.createdAt > $1.createdAt }) { run in RunRow(run: run).tag(SidebarItem.run(run.id)) } } } .navigationSplitViewColumnWidth(min: 220, ideal: 260) .toolbar { ToolbarItem(placement: .primaryAction) { Button { showNewRun = true } label: { Label("Nouveau run", systemImage: "plus") } .disabled(app.forgeError != nil) } } } detail: { switch selection { case .compare: CompareView() case .datasets: DatasetsView() case .run(let id): if let run = app.store.runs.first(where: { $0.id == id }) { RunDetailView(run: run) } else { EmptyStateView() } case nil: EmptyStateView() } } .sheet(isPresented: $showNewRun) { NewRunSheet() } } } struct RunRow: View { let run: Run var body: some View { VStack(alignment: .leading, spacing: 2) { HStack { Text(run.name).font(.headline) Spacer() StateBadge(state: run.state) } HStack(spacing: 8) { Text("\(run.config.model.paramCount.formatted(.number.notation(.compactName))) params") if let loss = run.lastTrainLoss { Text(String(format: "loss %.3f", loss)) } if run.state.isActive { ProgressView(value: run.progress).frame(width: 60) } } .font(.caption) .foregroundStyle(.secondary) } .padding(.vertical, 2) } } struct StateBadge: View { let state: RunState var color: Color { switch state { case .running, .launching: return .green case .finished: return .blue case .failed: return .red case .stopped: return .orange case .queued, .finishing: return .gray } } var body: some View { Text(state.rawValue) .font(.caption2.weight(.semibold)) .padding(.horizontal, 6) .padding(.vertical, 2) .background(color.opacity(0.18), in: Capsule()) .foregroundStyle(color) } } struct EmptyStateView: View { @Environment(AppModel.self) private var app var body: some View { VStack(spacing: 12) { Image(systemName: "flame") .font(.system(size: 48)) .foregroundStyle(.orange) Text("Forge Studio").font(.largeTitle.weight(.semibold)) if let device = app.forgeDevice { Label(device, systemImage: "cpu") .foregroundStyle(.secondary) } if let error = app.forgeError { Label(error, systemImage: "exclamationmark.triangle") .foregroundStyle(.red) SettingsLink { Text("Ouvrir les Réglages…") } } else { Text("Sélectionnez un run, ou créez-en un nouveau (+).") .foregroundStyle(.secondary) } } .frame(maxWidth: .infinity, maxHeight: .infinity) } }