From 91fbb0242fd10d5df2333ae6552c4e2dd5016853 Mon Sep 17 00:00:00 2001 From: David de Hilster Date: Fri, 18 Sep 2026 07:56:06 -0700 Subject: [PATCH] Try page: open on NLPFix's NLP analyzer, and offer every text Corporate comes off the try page, and NLP from NLPFix (nlpfix-analyzers/nlp, until now listed as "Entities") takes its place at the top, so the page opens on it. Seven analyzers, 145 files, 0.65 MB; all seven run clean. It is called NLP now, as its folder is, rather than a name made up for it. An analyzer can ship several texts to run on -- corporate has four, in two folders. The Text header now has a picker listing every file under the analyzer's input/, by path, starting on the one the build chose; choosing one replaces the box, and Reset puts the chosen text back. With one text there is nothing to choose and the picker does not appear -- which, for the seven shipped now, is every one of them. Which files are texts, and which to start on, is src/try/inputs.ts, pure and tested. TRY-PAGE.md says what changed, and no longer claims the shipped set exercises both pass-numbering edge cases: corporate was the one with a stub. Co-Authored-By: Claude Opus 5 (1M context) --- docs/TRY-PAGE.md | 35 +++++++++++++++++---------- studio/deploy/INSTALL.md | 2 +- studio/scripts/copy-analyzers.mjs | 14 +++++------ studio/src/try/inputs.ts | 23 ++++++++++++++++++ studio/src/try/main.ts | 27 ++++++++++++++++++--- studio/src/try/try.css | 3 +++ studio/test/inputs.test.ts | 40 +++++++++++++++++++++++++++++++ studio/try/index.html | 7 ++++-- 8 files changed, 125 insertions(+), 26 deletions(-) create mode 100644 studio/src/try/inputs.ts create mode 100644 studio/test/inputs.test.ts diff --git a/docs/TRY-PAGE.md b/docs/TRY-PAGE.md index 43bbc84..5b20c78 100644 --- a/docs/TRY-PAGE.md +++ b/docs/TRY-PAGE.md @@ -83,16 +83,16 @@ and a second copy of another repository besides. ## Which analyzers, and why -Eight of the twenty in the bundle, each showing something the others do not. Together they -come to 188 files and 0.78 MB — small enough to be fetched into a tab. +Seven of the twenty in the bundle, each showing something the others do not. Together they +come to 145 files and 0.65 MB — small enough to be fetched into a tab. The page opens on the +first, NLP from NLPFix. | On the page | From | Files | Size | Passes | Shows | |---|---|---|---|---|---| -| **Corporate** | `corporate` | 43 | 132K | 30 | Companies, money and events, with pronouns resolved back to what they refer to. The showcase analyzer. | +| **NLP** | `nlpfix-analyzers/nlp` | 26 | 314K | 17 | NLPFix's NLP analyzer: gathers what a text says about each person or thing into one record. Writes eight files. | | **Regions** | `nlp-tutorials/tutorial-07` | 10 | 38K | 5 | Where NLP++ code, functions and rules live in a pass — and what a code-only pass matches. | | **Dates and Times** | `nlpfix-analyzers/date-time` | 26 | 81K | 17 | Dates and times in many formats, where a regular expression needs rewriting for each. Four `.dict` files, and the only one that writes an `output.json`. | | **Formatting** | `nlpfix-analyzers/formatting` | 36 | 76K | 26 | Recovers headings, lists and tables from text that lost its formatting. | -| **Entities** | `nlpfix-analyzers/nlp` | 26 | 314K | 17 | Gathers what a text says about each person or thing into one record. Writes eight files. | | **Variables** | `nlp-tutorials/tutorial-02` | 17 | 49K | 12 | The five NLP++ variables — N, S, X, G and L — on a short résumé. | | **Pronouns** | `nlp-tutorials/tutorial-08` | 19 | 51K | 14 | Builds a knowledge base from the text as it reads, then resolves pronouns with it. | | **Ambiguity** | `nlp-tutorials/tutorial-15` | 11 | 53K | 4 | Choosing between readings of the same words, with a `.dict` and a `.kbb` file. | @@ -101,10 +101,9 @@ Two things about this set that are worth knowing before changing it. **Most of these analyzers have an empty knowledge base panel.** `` lists `.dict` and `.kbb` files and never the engine's own `.kb` files, which is correct and -is the rule the package exists to keep. But only *Dates and Times*, *Entities* and -*Ambiguity* ship any — Corporate, for all that it is the knowledge-base showcase, has six -`.kb` files and no `.dict` or `.kbb`, so its panel hides itself. That is the truth about -those analyzers, not a bug in the view. +is the rule the package exists to keep. But only *NLP*, *Dates and Times* and *Ambiguity* +ship any; the other tutorials carry only the engine's `.kb` files, so their panel hides +itself. That is the truth about those analyzers, not a bug in the view. **`` is not the answer here.** The reference implementation this page was modelled on leads with the fields an `output.json` filled. Of VisualText's analyzers only @@ -118,9 +117,10 @@ only when there is an `output.json` to show. |---|---| | `parse-en-us` | **Refused by the run server**: it calls `interactive()`, which `nlp_run.BLOCKED` lists alongside the desktop app's popups. See [§ Open questions](#open-questions) — this one is probably worth revisiting. 4.6 MB besides. | | `nlp-tutorials/tutorial-01` | 26 MB, over the run server's 8 MB `MAX_FILES_BYTES`, so it could be listed but never run. All of it is `kb/user/attr*.kb` and `word.kb` — engine `.kb` dumps, which are not listed in the knowledge base anyway — for a sequence of one `dicttokz` pass. The weakest demo in the set regardless. | -| `business` | Runs fine, and is one of the few analyzers with a visible knowledge base (2 `.dict`, 1 `.kbb`) — but it is **not in `analyzers.zip`**, only in the repository, so a build from a release cannot see it. It also covers much the same ground as Corporate. | +| `corporate` | Taken off the page by VisualText's choice, with NLPFix's NLP analyzer leading in its place. It runs fine, and is the one analyzer here with several texts (four, in `Dev/` and `Samples/`), so the text picker would show for it. | +| `business` | Runs fine, and is one of the few analyzers with a visible knowledge base (2 `.dict`, 1 `.kbb`) — but it is **not in `analyzers.zip`**, only in the repository, so a build from a release cannot see it. It also covers much the same ground as `corporate`. | | `files` | Walks a directory of files keeping a knowledge base across them. On a page that runs one text at a time, the point of it does not come across. | -| the other tutorials | They run, but each overlaps one of the eight above. `tutorial-13-b` reports 14 problems, which would exercise `` nicely but is not something to ship as a demo. | +| the other tutorials | They run, but each overlaps one of the seven above. `tutorial-13-b` reports 14 problems, which would exercise `` nicely but is not something to ship as a demo. | ### The survey this came from @@ -158,6 +158,7 @@ problems reported, `trees` parse trees kept. |---|---| | `studio/try/index.html` | the page: a top bar and three columns | | `studio/src/try/main.ts` | loading an analyzer, running it, and wiring the views' events | +| `studio/src/try/inputs.ts` | pure: which of an analyzer's files are texts to run on, and which to start with | | `studio/src/try/result.ts` | pure: turning a run result into what the views take, with tests in `studio/test/try.test.ts` | | `studio/src/try/try.css` | the three-column layout | | `studio/src/theme.css` | the palette and plain controls, shared with the studio page so a colour is changed once | @@ -169,6 +170,12 @@ never does: the sequence over the knowledge base on the left; whatever was click pass, a dictionary, a tree, an output file — in the middle; the text and the results on the right. Under 900px they stack. +**The text** starts as the analyzer's own sample: `input/text.txt`, or the first file in +`input/` where there is no such file. When an analyzer ships several texts, a picker in the +Text header offers every file under its `input/`, by path; choosing one replaces the box, +and **Reset** puts the chosen text back. With a single text the picker does not appear, and +none of the seven ships more than one (`src/try/inputs.ts`, tested in `test/inputs.test.ts`). + Every list is a `@visualtext/analyzer-views` element, which is the point: a pass is numbered and a knowledge base filtered here exactly as in the VS Code extension, because it is the same code. Nothing about an analyzer is drawn by hand. @@ -176,9 +183,11 @@ same code. Nothing about an analyzer is drawn by hand. ### Four things that will bite - **Pass numbering.** A switched-off pass (`/nlp foo`) keeps its number and writes no tree; - a `folder` or `stub` takes no number at all. `passes()` gets this right, and both cases - are live in the shipped set — `business` has the first, `corporate` the second — so - anything that renumbers by itself will mislabel every tree and every problem. `passTrees()` + a `folder` or `stub` takes no number at all. `passes()` gets this right. Neither case is + in the seven shipped now — `corporate` had a `stub`, `business` a switched-off pass — + but `test/try.test.ts` pins the gap a switched-off pass leaves, and anything that + renumbers by itself will mislabel every tree and every problem in an analyzer that has + one. `passTrees()` also keeps `final.tree` away from ``: it was written after the last pass but belongs to none, and handing it over puts the icons on whichever pass is numbered `null`. - **Mark the matches over the text that ran.** `ruleMatches(tree, text)` is given diff --git a/studio/deploy/INSTALL.md b/studio/deploy/INSTALL.md index e17fb26..5928144 100644 --- a/studio/deploy/INSTALL.md +++ b/studio/deploy/INSTALL.md @@ -219,7 +219,7 @@ The app can keep its sign-in settings; behind the password, people sign in with ## The try page `/studio/try/` is a second page in the same container, served from the same `dist` and -using the same `/studio/api/`. It lists eight of VisualText's own analyzers, baked into +using the same `/studio/api/`. It lists seven of VisualText's own analyzers, baked into the image at build time from a pinned `VisualText/analyzers` release, and lets whoever opens it run one on text they type. Nothing about it is separately installed or started: if `/studio/` works, it works. diff --git a/studio/scripts/copy-analyzers.mjs b/studio/scripts/copy-analyzers.mjs index 7cf5bf2..96ac30b 100644 --- a/studio/scripts/copy-analyzers.mjs +++ b/studio/scripts/copy-analyzers.mjs @@ -34,25 +34,25 @@ const studio = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const out = path.join(studio, "public", "try", "analyzers"); const repo = process.env.VISUALTEXT_ANALYZERS || path.resolve(studio, "..", "..", "analyzers"); -// Eight of the twenty in the bundle, each showing something the others do not, and each -// confirmed to run through the run server. What is left out, and why: +// Seven of the twenty in the bundle, each confirmed to run through the run server. The +// first is the one the page opens with. What is left out, and why: // +// corporate taken off by VisualText's choice; NLP from NLPFix leads +// instead. // parse-en-us refused: it calls interactive(), which nlp_run.BLOCKED // lists with the desktop app's popups. 4.6 MB besides. // nlp-tutorials/tutorial-01 26 MB of engine .kb dumps, over the run server's 8 MB // limit, for a sequence of one pass. -// the remaining tutorials they run, but each overlaps one of the eight below. +// the remaining tutorials they run, but each overlaps one of the seven below. const PICKED = [ - ["corporate", "Corporate", - "Companies, money and events, with pronouns resolved back to what they refer to."], + ["nlpfix-analyzers/nlp", "NLP", + "NLPFix's NLP analyzer: gathers what a text says about each person or thing into one record."], ["nlp-tutorials/tutorial-07", "Regions", "Where NLP++ code, functions and rules live in a pass — and what a code-only pass matches."], ["nlpfix-analyzers/date-time", "Dates and Times", "Dates and times in many formats, where a regular expression needs rewriting for each."], ["nlpfix-analyzers/formatting", "Formatting", "Recovers headings, lists and tables from text that lost its formatting."], - ["nlpfix-analyzers/nlp", "Entities", - "Gathers what a text says about each person or thing into one record."], ["nlp-tutorials/tutorial-02", "Variables", "The five NLP++ variables — N, S, X, G and L — on a short résumé."], ["nlp-tutorials/tutorial-08", "Pronouns", diff --git a/studio/src/try/inputs.ts b/studio/src/try/inputs.ts new file mode 100644 index 0000000..b786bf6 --- /dev/null +++ b/studio/src/try/inputs.ts @@ -0,0 +1,23 @@ +// The texts an analyzer ships to run on: every file under its input/. +// +// PURE: no DOM. An analyzer can carry several -- corporate has four, in two folders -- +// and the try page offers them all, starting on the one copy-analyzers.mjs chose. + +export interface InputText { + path: string; // input/Dev/Sold.txt, as the analyzer has it + label: string; // Dev/Sold.txt: what the picker shows +} + +const PREFIX = "input/"; + +// The texts, in path order. The one to start on is `chosen` when the analyzer has it, +// else the first; null when there are none, and the page starts with an empty box. +export function inputTexts(files: Iterable, chosen: string | null): + { texts: InputText[]; start: string | null } { + const texts = [...files] + .filter((path) => path.startsWith(PREFIX) && path.length > PREFIX.length) + .sort((a, b) => a.localeCompare(b)) + .map((path) => ({ path, label: path.slice(PREFIX.length) })); + const start = texts.some((t) => t.path === chosen) ? chosen : texts[0]?.path ?? null; + return { texts, start }; +} diff --git a/studio/src/try/main.ts b/studio/src/try/main.ts index 95711ea..7651100 100644 --- a/studio/src/try/main.ts +++ b/studio/src/try/main.ts @@ -17,6 +17,7 @@ import type { NlpCode, NlpKnowledgeBase, NlpLog, NlpOutput, NlpSequence, NlpTree import { ruleMatches } from "@visualtext/analyzer-views/rules"; import { fetchTree, outputFiles, runAnalyzer, serverHealth, type RunResult } from "../run/api"; +import { inputTexts } from "./inputs"; import { passTrees, resultTone, valuesOutput, viewTrees } from "./result"; import "./try.css"; @@ -54,6 +55,7 @@ const els = { pathNote: byId("path-note"), code: byId("code"), text: byId("text"), + inputPick: byId("input-pick"), resetText: byId("reset-text"), message: byId("result-message"), values: byId("values"), @@ -69,6 +71,7 @@ const state = { files: new Map(), // the analyzer's own files, by path result: null as RunResult | null, ranText: "", + input: null as string | null, // which of the analyzer's texts is in the box busy: false, }; @@ -165,11 +168,28 @@ async function openAnalyzer(entry: TryEntry): Promise { const at = release ?? (commit ? commit.slice(0, 8) : null); els.pinned.textContent = at ? `${repo}/${folder} at ${at}` : `${repo}/${folder}`; - els.text.value = entry.input ? state.files.get(entry.input) ?? "" : ""; - els.resetText.disabled = !entry.input; + // Its texts: a picker when there is more than one, none when there is a single text. + const { texts, start } = inputTexts(paths, entry.input); + els.inputPick.replaceChildren(...texts.map((t) => { + const option = document.createElement("option"); + option.value = t.path; + option.textContent = t.label; + return option; + })); + els.inputPick.hidden = texts.length < 2; + showInput(start); showAnalyzerFile("spec/analyzer.seq"); } +// One of the analyzer's texts into the box. Choosing another replaces what was typed: +// the box holds one text, and Reset puts the chosen one back. +function showInput(path: string | null): void { + state.input = path; + els.text.value = path ? state.files.get(path) ?? "" : ""; + if (path) els.inputPick.value = path; + els.resetText.disabled = !path; +} + // ---- running ---------------------------------------------------------------------- function showResult(result: RunResult): void { @@ -265,8 +285,9 @@ function wire(): void { if (entry) void openAnalyzer(entry); }); els.run.addEventListener("click", () => void run()); + els.inputPick.addEventListener("change", () => showInput(els.inputPick.value)); els.resetText.addEventListener("click", () => { - if (state.entry?.input) els.text.value = state.files.get(state.entry.input) ?? ""; + if (state.input) els.text.value = state.files.get(state.input) ?? ""; }); document.addEventListener("keydown", (e) => { if (e.key === "F5" && !e.ctrlKey && !e.metaKey) { e.preventDefault(); void run(); } diff --git a/studio/src/try/try.css b/studio/src/try/try.css index c2b59a7..22ad6c7 100644 --- a/studio/src/try/try.css +++ b/studio/src/try/try.css @@ -25,6 +25,9 @@ } .col-head .mono { overflow: hidden; text-overflow: ellipsis; } .col-head button { padding: 2px 8px; font-size: 12.5px; } +/* Which of the analyzer's texts: a long path is cut short rather than pushing Reset off. */ +.col-head .input-pick { padding: 2px 6px; font-size: 12.5px; min-width: 0; max-width: 60%; + text-overflow: ellipsis; } /* The lists, in this page's colours (analyzer-views reads these). */ .lists { diff --git a/studio/test/inputs.test.ts b/studio/test/inputs.test.ts new file mode 100644 index 0000000..eb3788e --- /dev/null +++ b/studio/test/inputs.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; + +import { inputTexts } from "../src/try/inputs"; + +describe("inputTexts", () => { + // corporate's input/, as the analyzers release has it. + const CORPORATE = [ + "spec/analyzer.seq", "kb/user/hier.kb", + "input/Samples/states.txt", "input/Dev/Sold.txt", + "input/Samples/companies.txt", "input/Samples/cities.txt", + ]; + + it("offers every file under input/, in path order, named without input/", () => { + expect(inputTexts(CORPORATE, "input/Dev/Sold.txt").texts).toEqual([ + { path: "input/Dev/Sold.txt", label: "Dev/Sold.txt" }, + { path: "input/Samples/cities.txt", label: "Samples/cities.txt" }, + { path: "input/Samples/companies.txt", label: "Samples/companies.txt" }, + { path: "input/Samples/states.txt", label: "Samples/states.txt" }, + ]); + }); + + it("starts on the text the build chose", () => { + expect(inputTexts(CORPORATE, "input/Samples/cities.txt").start).toBe("input/Samples/cities.txt"); + }); + + it("starts on the first when the chosen one is not there", () => { + expect(inputTexts(CORPORATE, "input/gone.txt").start).toBe("input/Dev/Sold.txt"); + expect(inputTexts(CORPORATE, null).start).toBe("input/Dev/Sold.txt"); + }); + + it("has one text for an analyzer with one, so the page need offer no choice", () => { + const { texts, start } = inputTexts(["spec/analyzer.seq", "input/text.txt"], "input/text.txt"); + expect(texts).toHaveLength(1); + expect(start).toBe("input/text.txt"); + }); + + it("has none, and nothing to start on, for an analyzer with no input/", () => { + expect(inputTexts(["spec/analyzer.seq", "spec/input.nlp"], null)).toEqual({ texts: [], start: null }); + }); +}); diff --git a/studio/try/index.html b/studio/try/index.html index e4f35b8..f2b91c5 100644 --- a/studio/try/index.html +++ b/studio/try/index.html @@ -45,8 +45,11 @@
- Text - + Text + + + +