File size: 6,479 Bytes
f737def
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f2a39b0
f737def
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f2a39b0
 
 
 
 
 
6d05aa6
f2a39b0
f737def
 
 
 
 
 
6d05aa6
f737def
 
 
 
 
 
 
6d05aa6
f737def
 
 
 
 
 
 
f05c576
f737def
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f7f3c2c
 
 
f737def
 
 
 
 
 
600100f
 
 
f737def
 
 
 
 
 
600100f
f737def
 
 
 
 
600100f
f7f3c2c
f737def
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import "server-only"

import fs from "node:fs"
import path from "node:path"

const TUTORIALS_DIR = path.join(process.cwd(), "content", "tutorials")
const SCREENSHOTS_DIR = path.join(process.cwd(), "public", "tutorials", "screenshots")

export type TutorialCategory = "guide" | "doc"

export interface TutorialMeta {
  slug: string
  title: string
  category: TutorialCategory
  /** Short "who it's for" (guides) or topic (docs) line. */
  audience: string
  /** One-sentence description of the focus. */
  blurb: string
}

/**
 * Ordered guide catalogue. Quickstart is stakeholder-agnostic and listed
 * first; the rest are the per-stakeholder guides. Copy mirrors the table in
 * the source tutorials README so the hub reads consistently with the guides.
 */
export const TUTORIALS: TutorialMeta[] = [
  {
    slug: "quickstart",
    title: "Quickstart",
    category: "guide",
    audience: "Stakeholder-agnostic · ~6 min",
    blurb:
      "The four signals, the five-level hierarchy, and your first five minutes on the site.",
  },
  {
    slug: "evaluation-researchers",
    title: "Evaluation researchers",
    category: "guide",
    audience: "Researchers auditing or comparing evaluations",
    blurb:
      "Researcher View, a per-section card walkthrough, rigorous signal reading, and comparability caveats.",
  },
  {
    slug: "policymakers",
    title: "Policymakers",
    category: "guide",
    audience: "Governance & policy",
    blurb:
      "What the evidence supports versus what it doesn't, independence checks, and responsible claims.",
  },
  {
    slug: "model-developers",
    title: "Model developers",
    category: "guide",
    audience: "Teams publishing models",
    blurb:
      "How your model is carded, what's documented versus missing, and a checklist to raise your signals.",
  },
  {
    slug: "general-public",
    title: "General public",
    category: "guide",
    audience: "Everyone",
    blurb: "Plain-language: what a benchmark score means and how to read a card.",
  },
  {
    slug: "journalists",
    title: "Journalists",
    category: "guide",
    audience: "Reporters",
    blurb: "The three-minute fact-check, sourcing, citation, and overclaiming traps.",
  },
]

/**
 * Technical documentation — deeper than the stakeholder guides. Rendered from
 * the same markdown pipeline and routed under /help/<slug>.
 */
export const DOCS: TutorialMeta[] = [
  {
    slug: "what-its-built-on",
    title: "What Evaluation Cards is built on",
    category: "doc",
    audience: "Data sources & structure",
    blurb:
      "The infrastructure behind the corpus: Auto-BenchmarkCards, Every Eval Ever, IBM Risk Atlas, and the five-level hierarchy.",
  },
  {
    slug: "how-signals-are-computed",
    title: "How the four signals are computed",
    category: "doc",
    audience: "Signal definitions",
    blurb:
      "The exact computation behind reproducibility, completeness, provenance, and comparability: fields, formulas, and corpus aggregation.",
  },
  {
    slug: "cross-post-to-hugging-face",
    title: "Cross-post EEE results to Hugging Face",
    category: "doc",
    audience: "Contributing evaluation data",
    blurb:
      "Send your Every Eval Ever results to Hugging Face Community Evals: the YAML schema, the converter, and the backlink to the full EEE record.",
  },
  {
    slug: "get-verified",
    title: "Get a verified checkmark",
    category: "doc",
    audience: "Verification",
    blurb:
      "Submit your data through your organization's Hugging Face account to have your results show up verified — our call for apples-to-apples comparison.",
  },
]

const ALL_ENTRIES = [...TUTORIALS, ...DOCS]

export function getTutorialSlugs(): string[] {
  return ALL_ENTRIES.map((t) => t.slug)
}

export function getTutorialMeta(slug: string): TutorialMeta | undefined {
  return ALL_ENTRIES.find((t) => t.slug === slug)
}

function listExistingScreenshots(): Set<string> {
  try {
    return new Set(fs.readdirSync(SCREENSHOTS_DIR))
  } catch {
    return new Set()
  }
}

/**
 * Reads a tutorial markdown file and returns it ready for rendering:
 * screenshot placeholder blocks are converted into image references. Where a
 * matching PNG exists in public/tutorials/screenshots it becomes a real image;
 * otherwise it becomes a `placeholder:<file>` reference the renderer styles as
 * a "screenshot coming" callout. Returns null if the slug has no file.
 */
export function readTutorial(slug: string): string | null {
  if (!getTutorialMeta(slug)) return null
  const filePath = path.join(TUTORIALS_DIR, `${slug}.md`)
  let raw: string
  try {
    raw = fs.readFileSync(filePath, "utf8")
  } catch {
    return null
  }
  return transformScreenshotBlocks(raw, listExistingScreenshots())
}

/**
 * Rewrites the `> 🖼️ **Screenshot — \`file.png\`** / *What to capture:* …`
 * blockquotes into markdown images (`.png` or `.gif`). Real screenshots resolve
 * to a public path; missing ones become `placeholder:` images so the renderer
 * can style them.
 */
function transformScreenshotBlocks(raw: string, existing: Set<string>): string {
  const lines = raw.split("\n")
  const out: string[] = []

  for (let i = 0; i < lines.length; i++) {
    // Allow leading whitespace: screenshot blocks nested under a list item are
    // indented (e.g. "  > 🖼️ …") and must still be matched.
    if (!/^\s*>\s*🖼️/.test(lines[i])) {
      out.push(lines[i])
      continue
    }

    // Collect the contiguous blockquote that starts with the screenshot marker.
    const block: string[] = []
    while (i < lines.length && /^\s*>/.test(lines[i])) {
      block.push(lines[i])
      i++
    }
    i-- // the outer loop will advance past the last consumed line

    const inner = block.map((l) => l.replace(/^\s*>\s?/, "")).join("\n")
    const file = inner.match(/`([^`]+\.(?:png|gif))`/)?.[1]
    if (!file) continue // malformed marker — drop it rather than render noise

    const rawCaption = inner.match(/\*What to capture:\*\s*([\s\S]+)$/)?.[1] ?? ""
    const caption = rawCaption
      .replace(/[*`<>]/g, "") // strip markdown emphasis / autolink / code markers
      .replace(/\s+/g, " ")
      .replace(/[[\]()]/g, "") // keep markdown image alt syntax intact
      .trim()
    const alt = caption || `Screenshot: ${file}`
    const src = existing.has(file) ? `/tutorials/screenshots/${file}` : `placeholder:${file}`

    out.push("", `![${alt}](${src})`, "")
  }

  return out.join("\n")
}