From 74294715bed777c3dc222e5adeb7f3ed3cdbcead Mon Sep 17 00:00:00 2001 From: David de Hilster Date: Fri, 23 Oct 2026 09:51:53 -0700 Subject: [PATCH] The studio shows the text a run reads, and a list to choose it from A Text pane beside the editor holds the input file the next run reads, editable, with a list of every input file above it, grouped by folder. It shares the input file's model, so an edit in either is the other's, and is kept, reverted and downloaded as before. The Text button hides and shows the pane, remembered in this browser. F5 runs from it too. Co-Authored-By: Claude Opus 5.5 --- studio/index.html | 8 +++++ studio/src/main.ts | 83 +++++++++++++++++++++++++++++++++++++++++++ studio/src/styles.css | 13 ++++++- 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/studio/index.html b/studio/index.html index c67b54b..ee2ae97 100644 --- a/studio/index.html +++ b/studio/index.html @@ -16,6 +16,7 @@ + @@ -48,6 +49,13 @@
+
+
+ + +
+
+
diff --git a/studio/src/main.ts b/studio/src/main.ts index cfecb33..60e4102 100644 --- a/studio/src/main.ts +++ b/studio/src/main.ts @@ -52,6 +52,8 @@ export const RUN_MARKERS = "nlp++ run"; const SAVE_AFTER_MS = 500; // Whether Debug was ticked last time, in this browser. const DEBUG_KEY = "nlp-studio.debug"; +// Whether the text pane was hidden last time, in this browser. +const TEXT_KEY = "nlp-studio.text-pane"; function remembered(key: string): string | null { try { @@ -74,6 +76,13 @@ function listedTree(file: TreeFile): ListedTree { return { name: file.name, pass: file.pass, passName: file.passName, bytes: file.size }; } +// Input files in the order the text list shows them: the input folder's own files first, +// then each folder's. +function byFolder(a: string, b: string): number { + const depth = (p: string) => (p.split("/").length > 2 ? 1 : 0); + return depth(a) - depth(b) || a.localeCompare(b, undefined, { numeric: true }); +} + function darkTheme(): boolean { const set = document.documentElement.dataset.theme; return set ? set === "dark" : window.matchMedia("(prefers-color-scheme: dark)").matches; @@ -103,6 +112,10 @@ export class Studio { passList: Pass[] = []; account: Account | null = null; readonly editor: monaco.editor.IStandaloneCodeEditor; + // The text a run reads, beside the editor: the same model as the input file, so an + // edit in either is the other's too, and is kept, reverted and downloaded the same. + readonly textEditor: monaco.editor.IStandaloneCodeEditor; + private textChoices = ""; readonly panel: RunPanel; readonly drafts = DraftStore.browser(); private readonly models = new Map(); @@ -153,6 +166,34 @@ export class Studio { }); monaco.editor.onDidChangeMarkers(() => this.showProblems()); + this.textEditor = monaco.editor.create(byId("text-editor"), { + automaticLayout: true, + fontSize: 13, + wordWrap: "on", + minimap: { enabled: false }, + scrollBeyondLastLine: false, + lineNumbersMinChars: 3, + theme: darkTheme() ? THEMES.dark : THEMES.light, + }); + this.textEditor.addAction({ + id: "nlp.runAnalyzer", + label: "Run Analyzer", + keybindings: [monaco.KeyCode.F5], + run: () => void this.run(), + }); + byId("text-pick").addEventListener("change", (event) => { + this.inputPath = (event.target as HTMLSelectElement).value || undefined; + this.showRunTarget(); + }); + const toggle = byId("text-toggle"); + toggle.setAttribute("aria-pressed", remembered(TEXT_KEY) === "hidden" ? "false" : "true"); + toggle.addEventListener("click", () => { + const shown = toggle.getAttribute("aria-pressed") !== "true"; + toggle.setAttribute("aria-pressed", String(shown)); + rememberSetting(TEXT_KEY, shown ? "shown" : "hidden"); + this.showText(); + }); + this.panel = new RunPanel(byId("results"), { open: (path, at) => this.openPath(path, at), openTree: (name) => void this.openTree(name), @@ -929,6 +970,48 @@ export class Studio { run.disabled = this.running || !name; run.textContent = this.running ? "Running…" : "Run"; run.classList.toggle("busy", this.running); + this.showText(); + } + + // The text pane: every input file to choose from, grouped by folder, and the chosen one + // to read and edit. Shown while the analyzer has input and the Text button is pressed. + private showText(): void { + const inputs = [...this.models.keys()].filter((p) => p.startsWith("input/")).sort(byFolder); + const shown = inputs.length > 0 && byId("text-toggle").getAttribute("aria-pressed") === "true"; + byId("text-pane").hidden = !shown; + byId("text-toggle").hidden = inputs.length === 0; + document.querySelector(".layout")?.classList.toggle("with-text", shown); + + const pick = byId("text-pick"); + const choices = inputs.join("\n"); + if (choices !== this.textChoices) { + this.textChoices = choices; + pick.replaceChildren(); + const groups = new Map(); + for (const path of inputs) { + const rel = path.slice("input/".length); + const cut = rel.lastIndexOf("/"); + const folder = cut < 0 ? "" : rel.slice(0, cut); + const option = new Option(cut < 0 ? rel : rel.slice(cut + 1), path); + if (this.handed.has(path)) option.textContent += " (handed in)"; + if (!folder) { + pick.append(option); + continue; + } + let group = groups.get(folder); + if (!group) { + group = document.createElement("optgroup"); + (group as HTMLOptGroupElement).label = folder; + groups.set(folder, group); + pick.append(group); + } + group.append(option); + } + } + pick.value = this.inputPath ?? ""; + pick.disabled = this.running; + const model = this.inputPath ? this.models.get(this.inputPath) ?? null : null; + if (this.textEditor.getModel() !== model) this.textEditor.setModel(model); } // ---- The file list --------------------------------------------------------------- diff --git a/studio/src/styles.css b/studio/src/styles.css index c8c67bf..ded2f6c 100644 --- a/studio/src/styles.css +++ b/studio/src/styles.css @@ -4,7 +4,12 @@ #analyzer { max-width: min(280px, 40vw); text-overflow: ellipsis; } .layout { flex: 1; display: grid; grid-template-columns: minmax(200px, 280px) 1fr; min-height: 0; } -@media (max-width: 720px) { .layout { grid-template-columns: 1fr; grid-template-rows: 30vh 1fr; } } +/* The text a run reads, beside the editor, while there is one to show. */ +.layout.with-text { grid-template-columns: minmax(200px, 280px) minmax(0, 1.4fr) minmax(260px, 1fr); } +@media (max-width: 720px) { + .layout, .layout.with-text { grid-template-columns: 1fr; grid-template-rows: 30vh 1fr; } + .layout.with-text { grid-template-rows: 30vh 1fr 40vh; } +} .files { overflow: auto; padding: 8px 10px; background: var(--panel); border-right: 1px solid var(--line); } /* The lists are @visualtext/analyzer-views (its style.css), in the studio's colours. */ @@ -48,6 +53,12 @@ button.commit { border-color: var(--accent); color: var(--on-ink); font-weight: .editor-pane { display: flex; flex-direction: column; min-width: 0; min-height: 0; } .path { padding: 5px 12px; font-size: 12px; color: var(--muted); border-bottom: 1px solid var(--line); background: var(--panel); } .editor { flex: 1; min-height: 0; } +.text-pane { display: flex; flex-direction: column; min-width: 0; min-height: 0; border-left: 1px solid var(--line); } +.text-pane[hidden] { display: none; } +.text-bar { display: flex; align-items: center; gap: 8px; padding: 3px 8px 3px 12px; border-bottom: 1px solid var(--line); + background: var(--panel); font-size: 12px; } +.text-bar select { flex: 1; min-width: 0; } +#text-toggle[aria-pressed="true"] { background: var(--on); color: var(--on-ink); } .problems { max-height: 22vh; overflow: auto; border-top: 1px solid var(--line); background: var(--panel); display: flex; flex-direction: column; } .problems button { border: 0; border-radius: 0; text-align: left; padding: 4px 12px; background: none; font-size: 12.5px; }