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%
9.1 KB · 234 lines swift
Raw Blame History
1//2//  ManualView.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import SwiftUI11import ZQEngine1213/// User manual pane: a searchable reference of every command (rendered14/// from the same registry as the console's `help`), plus an overview of15/// how Metrika works.16struct ManualPane: View {17    @State private var searchText = ""18    @State private var selection: String? = ManualPane.overviewID1920    static let overviewID = "__overview"2122    private var filteredDocs: [ZQCommandDoc] {23        let needle = searchText.trimmingCharacters(in: .whitespaces).lowercased()24        guard !needle.isEmpty else { return ZQCommandReference.all }25        return ZQCommandReference.all.filter {26            $0.verb.lowercased().contains(needle)27                || $0.summary.lowercased().contains(needle)28                || $0.category.lowercased().contains(needle)29        }30    }3132    var body: some View {33        HSplitView {34            VStack(spacing: 0) {35                HStack(spacing: 6) {36                    Image(systemName: "magnifyingglass")37                        .foregroundStyle(.secondary)38                    TextField("Search commands", text: $searchText)39                        .textFieldStyle(.plain)40                }41                .padding(8)42                Divider()43                List(selection: $selection) {44                    if searchText.isEmpty {45                        Label("How Metrika works", systemImage: "info.circle")46                            .tag(Self.overviewID)47                    }48                    ForEach(ZQCommandReference.categories, id: \.self) { category in49                        let docs = filteredDocs.filter { $0.category == category }50                        if !docs.isEmpty {51                            Section(category) {52                                ForEach(docs) { doc in53                                    Text(doc.verb)54                                        .fontDesign(.monospaced)55                                        .tag(doc.verb)56                                }57                            }58                        }59                    }60                }61                .listStyle(.sidebar)62            }63            .frame(minWidth: 200, maxWidth: 280)6465            ScrollView {66                Group {67                    if selection == Self.overviewID {68                        OverviewPage()69                    } else if let doc = ZQCommandReference.all.first(where: {70                        $0.verb == selection71                    }) {72                        CommandPage(doc: doc)73                    } else {74                        ContentUnavailableView(75                            "Select a command", systemImage: "book",76                            description: Text("Pick a topic from the list.")77                        )78                    }79                }80                .frame(maxWidth: 640, alignment: .leading)81                .padding(24)82            }83            .frame(maxWidth: .infinity)84        }85    }86}8788/// One command's reference page.89private struct CommandPage: View {90    let doc: ZQCommandDoc9192    var body: some View {93        VStack(alignment: .leading, spacing: 14) {94            HStack(alignment: .firstTextBaseline, spacing: 10) {95                Text(doc.verb)96                    .font(.title2.weight(.semibold))97                    .fontDesign(.monospaced)98                if let abbreviation = doc.abbreviation {99                    Text("abbreviation: \(abbreviation)")100                        .font(.caption)101                        .foregroundStyle(.secondary)102                }103                Spacer()104                Text(doc.category)105                    .font(.caption)106                    .padding(.horizontal, 8)107                    .padding(.vertical, 3)108                    .background(.quaternary, in: Capsule())109            }110111            Text(doc.summary)112113            GroupBox("Syntax") {114                Text(doc.syntax)115                    .fontDesign(.monospaced)116                    .font(.system(size: 12))117                    .textSelection(.enabled)118                    .frame(maxWidth: .infinity, alignment: .leading)119                    .padding(4)120            }121122            if !doc.options.isEmpty {123                GroupBox("Options") {124                    VStack(alignment: .leading, spacing: 6) {125                        ForEach(doc.options, id: \.name) { option in126                            HStack(alignment: .firstTextBaseline, spacing: 12) {127                                Text(option.name)128                                    .fontDesign(.monospaced)129                                    .font(.system(size: 12))130                                    .frame(width: 130, alignment: .leading)131                                Text(option.meaning)132                                    .font(.system(size: 12))133                            }134                        }135                    }136                    .frame(maxWidth: .infinity, alignment: .leading)137                    .padding(4)138                }139            }140141            if !doc.examples.isEmpty {142                GroupBox("Examples") {143                    VStack(alignment: .leading, spacing: 4) {144                        ForEach(doc.examples, id: \.self) { example in145                            Text(". \(example)")146                                .fontDesign(.monospaced)147                                .font(.system(size: 12))148                                .textSelection(.enabled)149                        }150                    }151                    .frame(maxWidth: .infinity, alignment: .leading)152                    .padding(4)153                }154            }155156            if let notes = doc.notes {157                Text(notes)158                    .font(.callout)159                    .foregroundStyle(.secondary)160            }161        }162    }163}164165/// "How Metrika works" — the architecture from a user's point of view.166private struct OverviewPage: View {167    var body: some View {168        VStack(alignment: .leading, spacing: 16) {169            Text("How Metrika works")170                .font(.title2.weight(.semibold))171172            section("The command line", """173            Everything in Metrika is a command, following one grammar:174175                command [varlist] [if condition] [in range] [, options]176177            Type commands in the Console pane (↑/↓ recall history), run whole \178            scripts from the Do-file pane (⌘R runs the selection or the file), \179            or filter the Data pane — its filter bar compiles to the same `if` \180            expressions. All three routes share one execution engine, so a \181            do-file replays exactly what you typed.182            """)183184            section("Data", """185            `use` loads parquet, csv, json, arrow, or Stata .dta files; `save` \186            writes them back. Data is columnar in memory (DuckDB underneath), \187            numeric values are 64-bit floats, and missing values are tracked \188            explicitly — every estimator reports how many observations \189            listwise deletion dropped. The sidebar always shows the working \190            dataset's variables and missing counts.191            """)192193            section("Estimation", """194            Estimators follow Stata conventions: `reg y x, robust \195            cluster(id)` gives HC1 or cluster-robust standard errors, factor \196            variables expand with `i.var`, and every CPU result is validated \197            against R to 1e-10. After fitting, `predict` generates fitted \198            values or residuals and `margins, dydx()` computes average \199            marginal effects with delta-method standard errors.200            """)201202            section("The GPU, invisibly", """203            A planner decides where each command runs — you never choose. \204            Large bootstrap runs execute batched on the Apple GPU; small jobs \205            stay on the CPU. Random numbers come from a counter-based Philox \206            generator, so `set seed 42` produces bit-identical resamples on \207            either backend, in any chunk order.208            """)209210            section("Extending Metrika", """211            Drop a .zyq script into ~/Library/Application Support/Metrika/\212            Commands/ and its filename becomes a command — an optional \213            `args name…` first line names the arguments, referenced as \214            `name' in the body. Native plugins are Swift types implementing \215            ZQCommandPlugin (see `help zscore` for the shipped example).216            """)217218            section("Reproducibility", """219            `log using file` records a session. Seeds are honored across CPU \220            and GPU. Do-files replay top to bottom with the errors pointing \221            at the failing line. The same engine powers the metrika-cli \222            terminal tool, so pipelines can run headless.223            """)224        }225    }226227    private func section(_ title: String, _ body: String) -> some View {228        VStack(alignment: .leading, spacing: 6) {229            Text(title).font(.headline)230            Text(body).font(.callout)231        }232    }233}234