// // ManualView.swift // Metrika // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // import SwiftUI import ZQEngine /// User manual pane: a searchable reference of every command (rendered /// from the same registry as the console's `help`), plus an overview of /// how Metrika works. struct ManualPane: View { @State private var searchText = "" @State private var selection: String? = ManualPane.overviewID static let overviewID = "__overview" private var filteredDocs: [ZQCommandDoc] { let needle = searchText.trimmingCharacters(in: .whitespaces).lowercased() guard !needle.isEmpty else { return ZQCommandReference.all } return ZQCommandReference.all.filter { $0.verb.lowercased().contains(needle) || $0.summary.lowercased().contains(needle) || $0.category.lowercased().contains(needle) } } var body: some View { HSplitView { VStack(spacing: 0) { HStack(spacing: 6) { Image(systemName: "magnifyingglass") .foregroundStyle(.secondary) TextField("Search commands", text: $searchText) .textFieldStyle(.plain) } .padding(8) Divider() List(selection: $selection) { if searchText.isEmpty { Label("How Metrika works", systemImage: "info.circle") .tag(Self.overviewID) } ForEach(ZQCommandReference.categories, id: \.self) { category in let docs = filteredDocs.filter { $0.category == category } if !docs.isEmpty { Section(category) { ForEach(docs) { doc in Text(doc.verb) .fontDesign(.monospaced) .tag(doc.verb) } } } } } .listStyle(.sidebar) } .frame(minWidth: 200, maxWidth: 280) ScrollView { Group { if selection == Self.overviewID { OverviewPage() } else if let doc = ZQCommandReference.all.first(where: { $0.verb == selection }) { CommandPage(doc: doc) } else { ContentUnavailableView( "Select a command", systemImage: "book", description: Text("Pick a topic from the list.") ) } } .frame(maxWidth: 640, alignment: .leading) .padding(24) } .frame(maxWidth: .infinity) } } } /// One command's reference page. private struct CommandPage: View { let doc: ZQCommandDoc var body: some View { VStack(alignment: .leading, spacing: 14) { HStack(alignment: .firstTextBaseline, spacing: 10) { Text(doc.verb) .font(.title2.weight(.semibold)) .fontDesign(.monospaced) if let abbreviation = doc.abbreviation { Text("abbreviation: \(abbreviation)") .font(.caption) .foregroundStyle(.secondary) } Spacer() Text(doc.category) .font(.caption) .padding(.horizontal, 8) .padding(.vertical, 3) .background(.quaternary, in: Capsule()) } Text(doc.summary) GroupBox("Syntax") { Text(doc.syntax) .fontDesign(.monospaced) .font(.system(size: 12)) .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) .padding(4) } if !doc.options.isEmpty { GroupBox("Options") { VStack(alignment: .leading, spacing: 6) { ForEach(doc.options, id: \.name) { option in HStack(alignment: .firstTextBaseline, spacing: 12) { Text(option.name) .fontDesign(.monospaced) .font(.system(size: 12)) .frame(width: 130, alignment: .leading) Text(option.meaning) .font(.system(size: 12)) } } } .frame(maxWidth: .infinity, alignment: .leading) .padding(4) } } if !doc.examples.isEmpty { GroupBox("Examples") { VStack(alignment: .leading, spacing: 4) { ForEach(doc.examples, id: \.self) { example in Text(". \(example)") .fontDesign(.monospaced) .font(.system(size: 12)) .textSelection(.enabled) } } .frame(maxWidth: .infinity, alignment: .leading) .padding(4) } } if let notes = doc.notes { Text(notes) .font(.callout) .foregroundStyle(.secondary) } } } } /// "How Metrika works" — the architecture from a user's point of view. private struct OverviewPage: View { var body: some View { VStack(alignment: .leading, spacing: 16) { Text("How Metrika works") .font(.title2.weight(.semibold)) section("The command line", """ Everything in Metrika is a command, following one grammar: command [varlist] [if condition] [in range] [, options] Type commands in the Console pane (↑/↓ recall history), run whole \ scripts from the Do-file pane (⌘R runs the selection or the file), \ or filter the Data pane — its filter bar compiles to the same `if` \ expressions. All three routes share one execution engine, so a \ do-file replays exactly what you typed. """) section("Data", """ `use` loads parquet, csv, json, arrow, or Stata .dta files; `save` \ writes them back. Data is columnar in memory (DuckDB underneath), \ numeric values are 64-bit floats, and missing values are tracked \ explicitly — every estimator reports how many observations \ listwise deletion dropped. The sidebar always shows the working \ dataset's variables and missing counts. """) section("Estimation", """ Estimators follow Stata conventions: `reg y x, robust \ cluster(id)` gives HC1 or cluster-robust standard errors, factor \ variables expand with `i.var`, and every CPU result is validated \ against R to 1e-10. After fitting, `predict` generates fitted \ values or residuals and `margins, dydx()` computes average \ marginal effects with delta-method standard errors. """) section("The GPU, invisibly", """ A planner decides where each command runs — you never choose. \ Large bootstrap runs execute batched on the Apple GPU; small jobs \ stay on the CPU. Random numbers come from a counter-based Philox \ generator, so `set seed 42` produces bit-identical resamples on \ either backend, in any chunk order. """) section("Extending Metrika", """ Drop a .zyq script into ~/Library/Application Support/Metrika/\ Commands/ and its filename becomes a command — an optional \ `args name…` first line names the arguments, referenced as \ `name' in the body. Native plugins are Swift types implementing \ ZQCommandPlugin (see `help zscore` for the shipped example). """) section("Reproducibility", """ `log using file` records a session. Seeds are honored across CPU \ and GPU. Do-files replay top to bottom with the errors pointing \ at the failing line. The same engine powers the metrika-cli \ terminal tool, so pipelines can run headless. """) } } private func section(_ title: String, _ body: String) -> some View { VStack(alignment: .leading, spacing: 6) { Text(title).font(.headline) Text(body).font(.callout) } } }