Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 22 additions & 13 deletions docs/TRY-PAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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.** `<nlp-knowledge-base>`
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.

**`<nlp-values>` 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
Expand All @@ -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 `<nlp-log>` 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 `<nlp-log>` nicely but is not something to ship as a demo. |

### The survey this came from

Expand Down Expand Up @@ -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 |
Expand All @@ -169,16 +170,24 @@ 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.

### 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 `<nlp-sequence>`: 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
Expand Down
2 changes: 1 addition & 1 deletion studio/deploy/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 7 additions & 7 deletions studio/scripts/copy-analyzers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 23 additions & 0 deletions studio/src/try/inputs.ts
Original file line number Diff line number Diff line change
@@ -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<string>, 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 };
}
27 changes: 24 additions & 3 deletions studio/src/try/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -54,6 +55,7 @@ const els = {
pathNote: byId<HTMLElement>("path-note"),
code: byId<NlpCode>("code"),
text: byId<HTMLTextAreaElement>("text"),
inputPick: byId<HTMLSelectElement>("input-pick"),
resetText: byId<HTMLButtonElement>("reset-text"),
message: byId<HTMLElement>("result-message"),
values: byId<NlpValues>("values"),
Expand All @@ -69,6 +71,7 @@ const state = {
files: new Map<string, string>(), // 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,
};

Expand Down Expand Up @@ -165,11 +168,28 @@ async function openAnalyzer(entry: TryEntry): Promise<void> {
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 {
Expand Down Expand Up @@ -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(); }
Expand Down
3 changes: 3 additions & 0 deletions studio/src/try/try.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
40 changes: 40 additions & 0 deletions studio/test/inputs.test.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
});
7 changes: 5 additions & 2 deletions studio/try/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@
<!-- The text to run over, and what the run did with it. -->
<section class="col right" aria-label="Text and results">
<div class="col-head">
<span class="grow">Text</span>
<button id="reset-text" type="button" title="Put the analyzer's own sample text back">Reset</button>
<span>Text</span>
<!-- Shown only when the analyzer ships more than one text to run on. -->
<select id="input-pick" class="input-pick" aria-label="Which of the analyzer's texts" hidden></select>
<span class="grow"></span>
<button id="reset-text" type="button" title="Put this text back as the analyzer has it">Reset</button>
</div>
<textarea id="text" class="text mono" spellcheck="false"
aria-label="The text to run the analyzer on"></textarea>
Expand Down
Loading