SPB Git

spb/metrika Public

Stata-class statistics, GPU-accelerated by Apple Silicon. Native Swift — no Electron, no Python runtime, no compromises.

Swift 92.4% HTML 3.3% R 3% Shell 1.3%
4.2 KB · 121 lines swift
Raw Blame History
1//2//  ConsoleView.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import SwiftUI1112/// Command console: scrollback of executed commands with their rendered13/// output, plus an input line with ↑/↓ history recall (CLAUDE.md §7).14struct ConsoleView: View {15    @Environment(SessionModel.self) private var model16    @State private var input = ""17    @State private var historyCursor: Int?18    @FocusState private var inputFocused: Bool1920    var body: some View {21        VStack(spacing: 0) {22            ScrollViewReader { proxy in23                ScrollView {24                    LazyVStack(alignment: .leading, spacing: 10) {25                        ForEach(model.entries) { entry in26                            VStack(alignment: .leading, spacing: 2) {27                                if !entry.command.isEmpty {28                                    Text(". \(entry.command)")29                                        .foregroundStyle(.secondary)30                                }31                                if !entry.output.isEmpty {32                                    Text(entry.output)33                                        .foregroundStyle(34                                            entry.isError ? Color.red : Color.primary35                                        )36                                        .textSelection(.enabled)37                                }38                            }39                            .id(entry.id)40                        }41                    }42                    .padding(12)43                    .frame(maxWidth: .infinity, alignment: .leading)44                }45                .onChange(of: model.entries) {46                    if let last = model.entries.last {47                        proxy.scrollTo(last.id, anchor: .bottom)48                    }49                }50            }5152            Divider()5354            HStack(spacing: 8) {55                Text(".")56                    .foregroundStyle(.secondary)57                TextField("Type a command — e.g. use sales.parquet", text: $input)58                    .textFieldStyle(.plain)59                    .focused($inputFocused)60                    .onSubmit(submit)61                    .onKeyPress(.upArrow) { recallHistory(step: -1) }62                    .onKeyPress(.downArrow) { recallHistory(step: 1) }63                if model.isRunning {64                    ProgressView()65                        .controlSize(.small)66                }67            }68            .padding(10)69        }70        .fontDesign(.monospaced)71        .font(.system(size: 12))72        .onAppear { inputFocused = true }73    }7475    private func submit() {76        let command = input77        input = ""78        historyCursor = nil79        Task { await model.run(command) }80    }8182    private func recallHistory(step: Int) -> KeyPress.Result {83        let history = model.history84        guard !history.isEmpty else { return .ignored }85        var cursor = historyCursor ?? history.count86        cursor = min(max(cursor + step, 0), history.count)87        historyCursor = cursor88        input = cursor < history.count ? history[cursor] : ""89        return .handled90    }91}9293/// Sidebar: variables of the working dataset with type and missing count94/// (CLAUDE.md §7, pane 5).95struct VariablesSidebar: View {96    @Environment(SessionModel.self) private var model9798    var body: some View {99        List {100            Section("Variables (\(model.observationCount) obs)") {101                ForEach(model.variables) { variable in102                    HStack {103                        Text(variable.name)104                            .fontDesign(.monospaced)105                        Spacer()106                        if variable.missingCount > 0 {107                            Text("\(variable.missingCount) mi")108                                .foregroundStyle(.orange)109                                .font(.caption)110                        }111                        Text(variable.type)112                            .foregroundStyle(.secondary)113                            .font(.caption)114                    }115                }116            }117        }118        .listStyle(.sidebar)119    }120}121