/* One template, as a notebook you can run. * * The prose is the point. These are teaching documents — the markdown cells * explain what is being modelled and why, and the code cells are the payoff. * So the page renders as a document first and only becomes an engine when * someone asks it to: the wasm front-end is ~100 MB, and downloading that to * read an explanation would be the wrong trade. * * Cells share one namespace, in order, exactly as in Jupyter. That is why * "Run" on cell 5 quietly runs 1–4 first if they have not run: a visitor who * clicks the interesting-looking cell should get its output, not a NameError * that teaches them nothing about Swan. */ import { renderMarkdown } from './markdown.js'; import { esc } from './util.js'; const MAX_ROWS = 50; /* `can` is whether this page offers to run the template. * * slow can: it does work, it just takes a while, and the note says so — that is * a decision for whoever is reading, not one to make for them. * * fails cannot: all four were checked natively and fail there identically, so * the Run button could only ever produce the error already printed in the note. * Offering it would be a button whose entire function is to disappoint. The * code and the prose are still worth reading, which is what "read only" means * on the card. */ const STATE = { ok: { tone: 'ok', can: true }, slow: { tone: 'warn', can: true }, /* needs-torch cannot run here by construction — torch publishes no * emscripten wheel — so offering Run would only produce the same refusal * every time. The prose and the code are the point on those pages. */ 'needs-torch': { tone: 'off', can: false }, fails: { tone: 'off', can: false }, 'no-notebook': { tone: 'off', can: false }, unmeasured: { tone: 'off', can: true }, }; /** Letters and digits only, so "Diet Optimization" matches "# Diet Optimisation:". */ const loosely = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, ''); /** * Strip a leading heading that only repeats the page title. * * Compared loosely, and only against the first heading: a template whose * opening section genuinely says something else keeps it. */ function dropTitleHeading(src, title) { const m = src.match(/^\s*#{1,3}\s+(.+?)\s*$/m); if (!m || loosely(m[1]) !== loosely(title)) return src; return src.replace(m[0], '').replace(/^\s*\n/, ''); } /** * Split a template README into its `##` sections. * * The READMEs are the copy RelationalAI wrote for these templates — what you * will build, what ships with it, what the output should look like — and until * now this page threw all of it away and showed the notebook alone. */ function splitReadme(src) { const lines = String(src || '').replace(/\r\n/g, '\n').split('\n'); const intro = []; const sections = []; let current = null; let fence = false; for (const line of lines) { if (/^\s*```/.test(line)) fence = !fence; // Only outside a fence: "## Something" inside a code block is a comment. const h = !fence && line.match(/^##\s+(.+?)\s*$/); if (h) { current = { title: h[1], body: [] }; sections.push(current); continue; } if (current) current.body.push(line); else if (!/^#\s/.test(line)) intro.push(line); } return { intro: intro.join('\n').trim(), sections: sections.map((s) => ({ title: s.title, body: s.body.join('\n').trim() })), }; } /* What to do with each `##` section of a README. * * Counted across the corpus rather than guessed from one template — the common * sections are "What this template is for" (38), "Customize this template" * (37), "Template structure" (36), "How it works" (36), "Quickstart" (33), * "What you'll build" (30), "Troubleshooting" (30), "What's included" (29). * Anything not named below is kept, so a section this list has never seen * still reaches the page. * * SKIPPED are the sections about running it somewhere else. Prerequisites says * to install Python and `make` the Swan extension; Quickstart says to `cd` into * the template directory and launch Jupyter. Both are true of a local checkout * and both are precisely what this page removes, so printing them verbatim * would contradict the thing the page exists to demonstrate. One sentence * replaces them. * * COLLAPSED are long and secondary. The walkthrough narrates the same code the * page renders live below — my first cut dropped it as a duplicate, which was * wrong for exactly the templates that need it most: three notebooks have one * markdown cell or none, so the explanation of *why* each step exists is in * that section and nowhere else. Open by default it would dwarf the notebook; * gone, a sparsely commented notebook has nothing to read alongside it. */ /* The numbered form matters: ten READMEs promote the Quickstart steps to `##` * headings, so the section is literally titled "1. Navigate to the template * directory:" and carries the `cd` and `jupyter notebook` lines with it. Matched * by name rather than by "starts with a digit", because eleven other numeric * headings across the corpus are real content — "3. Prescriptive Knapsack * Optimization", "2. Smurf Army Model" — and dropping those would be worse than * the leak. */ const SKIPPED = /^(prerequisites|quickstart|quick start|getting started|installation|setup|running the (template|notebook)|\d+[.)]\s*(navigate|install|activate|create a virtual|run the notebook))/i; /* Shown open: the sections that answer "what is this and should I care". * * Everything else in the README is kept but collapsed. That distinction is the * difference between a page and a wall — telco_network_recovery has fourteen * sections, and rendering them all open produced a 16,000-pixel page where the * runnable notebook, the entire point, began below eight thousand pixels of * prose. Nothing is discarded; the reference material simply starts folded. */ const PRIMARY = /^(what this template is for|who this is for|what you'?ll build|what'?s included|how it works|overview|about)/i; /* Collapsed next to the notebook rather than up in the overview, because both * are about the code directly below them. */ const NEAR_CODE = /^((code )?walkthrough|troubleshooting)/i; const WALKTHROUGH = /^(code )?walkthrough/i; /** Distinctive identifiers in a chunk of Python, for matching prose to a cell. */ function codeFingerprint(text) { const tokens = String(text).match(/[A-Za-z_][A-Za-z0-9_]{3,}/g) || []; // Words that appear in every cell of every template carry no signal. const common = new Set(['import', 'from', 'model', 'self', 'None', 'True', 'False', 'print', 'return', 'for', 'name', 'data', 'this', 'with']); return new Set(tokens.filter((t) => !common.has(t))); } /** Jaccard overlap, 0..1. */ function similarity(a, b) { if (!a.size || !b.size) return 0; let shared = 0; for (const t of a) if (b.has(t)) shared += 1; return shared / (a.size + b.size - shared); } /** * Split a Code Walkthrough into its `###` steps, and work out which notebook * cell each one is describing. * * Matched on the code itself rather than on position. The walkthrough is not * cell-by-cell — ad-spend-allocation's has two steps for eight cells, and the * numbering restarts and skips — so pairing them in order would confidently * caption the wrong cell, which is worse than captioning none. * * Each step quotes the code it is talking about, so its fenced block is * fingerprinted and compared against every cell. A step is only attached where * one cell is a clear best match; anything unmatched stays in the folded block * so no prose is lost. */ function walkthroughSteps(body, cells) { const lines = String(body).split('\n'); const steps = []; let current = null; let fence = false; for (const line of lines) { if (/^\s*```/.test(line)) fence = !fence; const h = !fence && line.match(/^#{3,4}\s+(.+?)\s*$/); if (h) { current = { title: h[1], body: [] }; steps.push(current); continue; } if (current) current.body.push(line); } const MIN_SCORE = 0.12; const taken = new Set(); const fingerprints = cells.map((c) => codeFingerprint(c.original)); for (const step of steps) { step.body = step.body.join('\n').trim(); // Setup steps ("1. Navigate to the template directory") describe a shell, // not a cell — the same instructions dropped from Quickstart. if (SKIPPED.test(step.title)) { step.skip = true; continue; } const fenced = [...step.body.matchAll(/```[a-z]*\n([\s\S]*?)```/g)].map((m) => m[1]).join('\n'); if (!fenced.trim()) continue; const want = codeFingerprint(fenced); let best = -1; let bestScore = 0; fingerprints.forEach((fp, i) => { if (taken.has(i)) return; const score = similarity(want, fp); if (score > bestScore) { bestScore = score; best = i; } }); if (best >= 0 && bestScore >= MIN_SCORE) { step.cell = best; taken.add(best); } } return steps; } const EXPECTED_SECTION = /^expected/i; export function createTemplate({ doc, engine, root = document.body }) { const el = build(root, doc); const codeCells = []; let prepared = null; const status = STATE[doc.status] || STATE.unmeasured; const { nearCode } = renderOverview(el, doc); renderFiles(el, doc); renderLinks(el, doc); /* Render the notebook. Markdown becomes prose; code becomes a cell with its * own bar, its own Run and its own output. */ let firstProse = true; for (const cell of doc.cells) { if (cell.kind === 'markdown') { const div = document.createElement('div'); div.className = 'tt-prose'; /* Nearly every notebook opens with a heading restating its own title, * which the page has already shown in larger type directly above. Two * identical headings one after the other read as a rendering bug. */ div.innerHTML = renderMarkdown(firstProse ? dropTitleHeading(cell.source, doc.title) : cell.source); firstProse = false; if (div.textContent.trim()) el.cells.append(div); continue; } const index = codeCells.length; const box = document.createElement('div'); box.className = 'tt-cell'; /* A textarea, not a
. Reading a template teaches less than changing
     * one: raise the budget, drop a constraint, and watch the solver disagree
     * with you. The engine already runs whatever string it is handed — the
     * page was the only thing insisting the string could not change. */
    box.innerHTML = `
      
cell ${index + 1}
`; const code = box.querySelector('.tt-code'); code.value = cell.source; const entry = { index, original: cell.source, get source() { return code.value; }, box, code, run: box.querySelector('.tt-run'), reset: box.querySelector('.tt-reset'), time: box.querySelector('.tt-cell-time'), out: box.querySelector('.tt-out'), done: false, }; /* Grow to fit. A textarea defaults to a fixed number of rows and its own * scrollbar, which for a 60-line cell means reading code through a letterbox * — worse than the
 this replaced. */
    const fit = () => {
      code.style.height = 'auto';
      code.style.height = `${code.scrollHeight}px`;
    };
    // Not called yet: scrollHeight is 0 for an element that is not in the
    // document, so sizing here left every cell one line tall. Called after the
    // append below, where the box has a layout.
    entry.fit = fit;

    code.addEventListener('input', () => {
      fit();
      const edited = code.value !== entry.original;
      entry.reset.hidden = !edited;
      box.classList.toggle('is-edited', edited);
      /* An edited cell has not been run in its current form, and neither have
       * the cells after it — they ran against what this one used to define. */
      if (edited) for (const c of codeCells.slice(index)) c.done = false;
    });

    // ⌘/ctrl + enter, the same shortcut the query page's editor uses.
    code.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        if (!entry.run.disabled) runTo(index);
      }
    });

    entry.reset.addEventListener('click', () => {
      code.value = entry.original;
      fit();
      entry.reset.hidden = true;
      box.classList.remove('is-edited');
    });

    entry.run.title = 'Runs any earlier cells that have not run yet — ⌘/ctrl + enter';
    entry.run.addEventListener('click', () => runTo(index));
    if (!status.can) { entry.run.disabled = true; code.readOnly = true; }
    codeCells.push(entry);
    el.cells.append(box);
    fit();
  }

  /* Three templates ship no notebook, so there is nothing to render and the
   * page would be a title over blank space. Their README still explains what
   * the template does, which is most of what a reader came for — show it rather
   * than leaving a dead page behind a card that promised something. */
  if (!codeCells.length && doc.readme) {
    const div = document.createElement('div');
    div.className = 'tt-prose';
    div.innerHTML = renderMarkdown(dropTitleHeading(doc.readme, doc.title));
    el.cells.append(div);
  }

  /* Now that the cells exist, hand the walkthrough its steps.
   *
   * Deliberately after the loop above: each step is placed against the cell it
   * describes, which cannot be worked out before the cells are built. What is
   * left over — steps that match no cell, and Troubleshooting — stays folded
   * above the notebook.
   */
  placeWalkthrough(el, nearCode, codeCells);

  el.notebookHead.hidden = codeCells.length === 0;
  el.runAll.disabled = !status.can || codeCells.length === 0;
  if (el.runAll.disabled) {
    el.runAll.title = doc.note || '';
    // A button that can never do anything is noise on a page about reading.
    el.runAll.hidden = true;
  }

  function setBusy(busy, text) {
    el.runAll.disabled = busy || !status.can || !codeCells.length;
    for (const c of codeCells) c.run.disabled = busy || !status.can;
    el.progress.textContent = text || '';
  }

  /** Boot the engine on first use, and hand the worker this template's data. */
  async function prepare() {
    if (prepared) return prepared;
    prepared = (async () => {
      // Pick a backend before asking it for anything: the engine façade throws
      // "engine not ready" rather than booting on demand.
      await engine.ready();

      // csv path -> table, both as written and by basename: notebooks reference
      // "data/foo.csv" and "foo.csv" interchangeably, sometimes in one file.
      const tables = {};
      for (const t of doc.tables || []) {
        tables[t.csv] = t.table;
        tables[t.csv.split('/').pop()] = t.table;
      }
      await engine.prepareTemplate({
        slug: doc.slug,
        database: doc.database || null,
        tables,
        packages: doc.packages || [],
      });
    })().catch((err) => { prepared = null; throw err; });
    return prepared;
  }

  function show(cell, { stdout, result, error }) {
    cell.out.textContent = '';
    cell.out.hidden = !(stdout || result || error);

    if (stdout) {
      const p = document.createElement('pre');
      p.className = 'tt-stdout';
      p.textContent = stdout.replace(/\s+$/, '');
      cell.out.append(p);
    }
    if (error) {
      const p = document.createElement('pre');
      p.className = 'tt-error';
      p.textContent = error;
      cell.out.append(p);
      return;
    }
    if (!result) return;

    if (result.kind === 'frame') {
      const table = document.createElement('table');
      table.className = 'tt-table';
      const thead = document.createElement('thead');
      const tr = document.createElement('tr');
      for (const c of result.columns) {
        const th = document.createElement('th');
        th.textContent = c;
        tr.append(th);
      }
      thead.append(tr);
      table.append(thead);
      const tbody = document.createElement('tbody');
      for (const row of result.rows) {
        const r = document.createElement('tr');
        for (const v of row) {
          const td = document.createElement('td');
          td.textContent = v === null || v === undefined ? '' : String(v);
          r.append(td);
        }
        tbody.append(r);
      }
      table.append(tbody);
      cell.out.append(table);
      if (result.total > MAX_ROWS) {
        const more = document.createElement('div');
        more.className = 'tt-more';
        more.textContent = `showing ${MAX_ROWS} of ${result.total.toLocaleString()} rows`;
        cell.out.append(more);
      }
      return;
    }
    const p = document.createElement('pre');
    p.className = 'tt-value';
    p.textContent = result.text;
    cell.out.append(p);
  }

  /** Run one cell. Returns true if it succeeded. */
  async function runOne(cell) {
    cell.box.classList.remove('is-done', 'is-failed');
    cell.box.classList.add('is-running');
    cell.time.textContent = 'running…';
    const started = performance.now();
    try {
      const res = await engine.runTemplateCell(cell.source);
      const ms = performance.now() - started;
      show(cell, res);
      cell.done = !res.error;
      // Something has run, so there may now be a model to read. The section
      // only offers the button; nothing is built until it is pressed.
      if (!res.error) el.model.hidden = false;
      cell.box.classList.toggle('is-done', !res.error);
      cell.box.classList.toggle('is-failed', Boolean(res.error));
      cell.time.textContent = `${ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`}`;
      return !res.error;
    } catch (err) {
      // A thrown error is the worker or the engine failing, not the cell —
      // worth distinguishing, because the fix is different.
      show(cell, { error: `the engine failed: ${err && err.message ? err.message : err}` });
      cell.box.classList.add('is-failed');
      cell.time.textContent = '';
      return false;
    } finally {
      cell.box.classList.remove('is-running');
    }
  }

  /** Run everything up to and including `index` that has not run yet. */
  async function runTo(index) {
    setBusy(true, 'starting Swan…');
    try {
      await prepare();
    } catch (err) {
      setBusy(false, '');
      const cell = codeCells[index];
      show(cell, { error: `could not start: ${err && err.message ? err.message : err}` });
      cell.box.classList.add('is-failed');
      return;
    }
    const pending = codeCells.slice(0, index + 1).filter((c) => !c.done);
    for (const cell of pending) {
      setBusy(true, `running cell ${cell.index + 1} of ${codeCells.length}…`);
      cell.box.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
      // Stop at the first failure: later cells depend on this one, and a run
      // of cascading NameErrors hides the error that actually mattered.
      if (!await runOne(cell)) {
        setBusy(false, `stopped at cell ${cell.index + 1}`);
        return;
      }
    }
    setBusy(false, pending.length ? 'done' : 'already run');
  }

  el.runAll.addEventListener('click', () => runTo(codeCells.length - 1));

  /* ---- the model, once there is one ------------------------------------
   *
   * Offered only after a cell has run, and built only when asked. Reading the
   * model means fetching ~100 KB of Python and walking the whole catalog; a
   * reader who came to run the notebook should not pay for it, and before the
   * cells run there is nothing to read — the namespace is empty and the honest
   * answer would be an empty graph, which reads as a broken feature rather
   * than as "nothing has happened yet".
   */
  let ladder = null;
  async function showModel() {
    if (ladder) return;
    el.modelShow.disabled = true;
    el.modelNote.textContent = 'reading the model…';
    try {
      const { graph, models } = await engine.templateGraph();
      if (!graph) {
        // Not a failure: plenty of templates query data without declaring a
        // model at all, and saying so beats an empty diagram.
        el.modelNote.textContent = models
          ? 'the cells built a model this reader could not read'
          : 'this template does not declare a model — it works directly against the tables';
        el.modelShow.hidden = true;
        return;
      }
      const { createLadder } = await import('./ladder.js');
      ladder = await createLadder({
        graph, container: el.modelLadder, embedded: true, navHost: el.modelNav,
      });
      el.modelLadder.hidden = false;
      el.modelShow.hidden = true;
      el.model.classList.add('is-open');
      const counts = (graph.meta && graph.meta.counts) || {};
      const bits = ['concepts', 'rules', 'tables']
        .filter((k) => counts[k])
        .map((k) => `${counts[k]} ${counts[k] === 1 ? k.replace(/s$/, '') : k}`);
      /* Most templates in the corpus are optimisation problems and declare no
       * rules at all. There is still a model worth seeing — concepts and the
       * tables they map to — but calling that a derivation would be a promise
       * the view cannot keep, so it says which one it is showing. */
      el.modelNote.textContent = bits.join(' · ')
        + (counts.rules ? '' : ' — no derived values, so this shows how the concepts map to tables');
    } catch (err) {
      // Same reasoning as the query page: a panel that will not build must not
      // take the notebook with it.
      el.modelNote.textContent = `the model could not be read — ${err && err.message || err}`;
      el.modelShow.disabled = false;
    }
  }

  el.modelShow.addEventListener('click', showModel);

  /* Hand the session back when the page goes. The worker is shared and
   * outlives this page; without this its namespace, its model and its open
   * connection sit there until the session cap evicts them. `pagehide` rather
   * than `unload`, which does not fire reliably and blocks the bfcache. */
  window.addEventListener('pagehide', () => {
    if (prepared) { try { engine.closeTemplate(); } catch { /* going away anyway */ } }
  }, { once: true });

  return { doc, runTo, cells: codeCells, showModel };
}

/**
 * Take the setup talk out of the opening sentence.
 *
 * The READMEs open with "designed to help you get up and running with **Swan**
 * (PyRel over DuckDB) locally, implementing marketing budget optimization…".
 * Half of that sentence is about installing a library, and it lands before the
 * reader has been told what the template *does* — on a page where the thing is
 * already running, it is noise in the most valuable position on the page.
 *
 * Only this one construction, and only its middle: the clause after it is the
 * author's own description and is kept word for word.
 */
const SETUP_TALK = /\bdesigned to help you get up and running with\s+\*{0,2}Swan\*{0,2}\s*(?:\([^)]*\))?\s*locally,\s*(?:implementing\s+)?/i;
const plainer = (s) => s.replace(SETUP_TALK, 'designed to help you implement ');

/**
 * README sections as folded blocks.
 *
 * `
`, so the open/closed state costs no JavaScript. The hint is built * before the template literal rather than inlined: an inline ternary there * reads badly, and check_escaping.mjs cannot distinguish a constant-string * choice from an unescaped interpolation — it flagged one, correctly, on the * evidence available to it. */ function collapsibles(sections, hintFor = () => '') { return sections.map((s) => { const hint = hintFor(s.title); return `
${esc(s.title)}${esc(hint)}
${renderMarkdown(s.body)}
`; }).join(''); } /** The README's own explanation of the template, minus what this page replaces. */ function renderOverview(el, doc) { const { intro, sections } = splitReadme(doc.readme); const usable = sections.filter((s) => !SKIPPED.test(s.title) && !EXPECTED_SECTION.test(s.title)); const nearCode = usable.filter((s) => NEAR_CODE.test(s.title)); /* Open if it is orienting *and* short. * * Name alone was not enough. "How it works" is a primary section by any * reading, and on 27 of the 36 templates that have one it runs past 1,800 * characters — telco's is 3,001, where its four sibling sections total 3,100 * between them. Expanding it by name put the notebook 4,200 pixels down the * page. The other orienting sections are 500-950 characters and belong open, * so the length is what separates them, not the title. */ const LONG = 1800; const keep = usable.filter((s) => PRIMARY.test(s.title) && !NEAR_CODE.test(s.title) && s.body.length <= LONG); const rest = usable.filter((s) => !NEAR_CODE.test(s.title) && !keep.includes(s)); const expected = sections.find((s) => EXPECTED_SECTION.test(s.title)); if (intro || keep.length) { const parts = []; if (intro) parts.push(renderMarkdown(plainer(intro))); for (const s of keep) { parts.push(`

${esc(s.title)}

`, renderMarkdown(s.body)); } /* Stands in for the Prerequisites section that was dropped. Saying what is * *not* needed is the clearest statement of what this page is for, and it * belongs next to the copy that would otherwise have asked for it. */ /* Stands in for the Prerequisites and Quickstart sections that were * dropped, and covers what is left. A few kept sections still say things * like "**Start here**: run `jupyter notebook x.ipynb`" mid-paragraph; * those are RelationalAI's words about their own artifact, and editing * inside their sentences would be worse than one note explaining that the * setup they describe is not needed on this page. */ parts.push( '

This copy comes from the template\'s own README, so it describes ' + 'running it locally — Python, a build of the Swan extension, jupyter notebook. ' + 'None of that is needed here: the engine below is Swan compiled to WebAssembly, and it ' + 'runs in this tab.

', ); el.overview.innerHTML = `

About this template

${parts.join('\n')}
`; el.overview.hidden = false; } // The rest of the README — model overview, sample data, how to customise it, // structure, further reading. Folded, in the order the author wrote them. if (rest.length) { el.more.innerHTML = collapsibles(rest); el.more.hidden = false; } return { nearCode }; if (expected) { el.expected.innerHTML = `

${esc(expected.title)}

` + `
${renderMarkdown(expected.body)}
`; el.expected.hidden = false; } } /** * Put each walkthrough step above the cell it explains. * * The point of the exercise: a reader scrolling the notebook should meet the * explanation of a cell immediately before the cell, not have to hold a block * of numbered steps in their head from the top of the page. */ function placeWalkthrough(el, sections, cells) { const leftovers = []; for (const section of sections) { if (!WALKTHROUGH.test(section.title) || !cells.length) { leftovers.push(section); continue; } const steps = walkthroughSteps(section.body, cells); for (const step of steps.filter((s) => s.cell !== undefined)) { /* If the notebook already explains this cell, leave it alone. * * simple-start has both: markdown cells titled "2. Define Semantic * Schema" and a walkthrough step of the same name, saying the same thing * in different words. Inserting the step produced the heading twice, one * above the other, which reads as a rendering fault. The notebook's own * prose wins — it was written next to the cell, and it is what a reader * of the .ipynb would see. */ const previous = cells[step.cell].box.previousElementSibling; if (previous && previous.classList.contains('tt-prose')) { step.cell = undefined; continue; } const note = document.createElement('div'); note.className = 'tt-prose tt-step'; /* The step quotes the cell's code, and the cell is now directly beneath * it — printing the same lines twice only pushes them apart. */ const prose = step.body.replace(/```[a-z]*\n[\s\S]*?```/g, '').trim(); note.innerHTML = `

${esc(step.title)}

${renderMarkdown(prose)}`; cells[step.cell].box.before(note); } const rest = steps.filter((s) => s.cell === undefined && !s.skip && s.body.trim()); if (rest.length) { leftovers.push({ title: section.title, body: rest.map((s) => `### ${s.title}\n\n${s.body}`).join('\n\n'), }); } } if (leftovers.length) { el.walkthrough.innerHTML = collapsibles(leftovers, (title) => (/troubleshoot/i.test(title) ? '' : ' — the steps that did not line up with a single cell')); el.walkthrough.hidden = false; } } /** * The data the template is grounded in, as files you can open. * * The engine never fetches these — it loads the .duckdb and writes the rows out * itself. They are here because "what is actually in channels.csv" is the first * question anyone reading a semantic model asks, and answering it with a link * beats answering it with a paragraph. */ function renderFiles(el, doc) { const tables = (doc.tables || []).filter((t) => t.file); if (!tables.length) return; const rows = tables.map((t) => ` ${esc(t.csv)} ${Number(t.rows || 0).toLocaleString()} ${(t.columns || []).map((c) => `${esc(c)}`).join(' ')} `).join(''); el.files.innerHTML = `

Data

The template's own CSVs. The notebook reads them as data/<name>.csv; in this tab they are served out of a DuckDB file built from exactly these bytes.

${rows}
FileRowsColumns
`; el.files.hidden = false; } /** Out to RelationalAI's own page for this template, where one exists. */ function renderLinks(el, doc) { if (!doc.docs) return; el.links.innerHTML = ``; el.links.hidden = false; } function build(root, doc) { root.classList.add('hz-root', 'tp-root'); const status = STATE[doc.status] || STATE.unmeasured; const chips = [doc.industry, doc.experience_level, ...(doc.reasoning_types || []), ...(doc.tags || [])] .filter(Boolean); root.innerHTML = `
RelationalAI

${esc(doc.title)}

${esc(doc.blurb || '')}

${chips.map((c) => `${esc(c)}`).join('')}
${esc(doc.note || '')}
`; const el = { root }; for (const node of root.querySelectorAll('[data-el]')) el[node.dataset.el] = node; return el; }