diff --git a/ChangeLog b/ChangeLog index a83bb3cf..c204105e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -14,6 +14,9 @@ ## Interface and handling +- reorganize GUI parameters, add conditional seed/accessibility controls and + grouped CSV defaults, expand FASTA limits and refine result headers (PR #299) + - retain computed GUI runs as named, foldable sessions with JSON input restoration, independent outputs, per-session ZIP export and deletion (PR #297 review) @@ -282,6 +285,27 @@ energies, restricted partition sums, and trackers. ################################################################################ ################################################################################ +261024 Alexander Mitrofanov + * gui/{app,options,columns}.js, gui/{index.html,styles.css} + * organize tabs/rows as Sequences, Seed, Output, Model & Energy, Query, + Target and CLI-only; align wrapped labels and place Compute first + + add a three-way seed selector, synchronized accessibility flags and + mode-dependent input availability; place SHAPE beside accessibility + * group compact CSV choices with descriptions and group selection actions; + default to 10 predictions and expanded task-specific energy columns, + explicitly preserving browser defaults in native-compatible JSON + * gui/{protocol,uploads,parameter-file}.js, gui/examples/ + * accept 50 FASTA records per strand of 2-5,000 nt each, with longest + selected query x longest selected target <= 1,000,000; allow 1 MB FASTA + + warn about ignored FASTA header descriptions while using first-token IDs + * gui/{sessions,results}.js + * select results from the header/name, order Show/Hide before the name and + ZIP/delete actions, compact table headings and distinguish selected rows + * gui/tests/, scripts/wasm/test-browser.py, README.md, gui/docs/ + + cover limits, native JSON replay/defaults, dependent controls, grouped CSV + choices, desktop/mobile alignment and result header selection + * address https://github.com/BackofenLab/IntaRNA/pull/299#issuecomment-6375752155 + 261023 Alexander Mitrofanov * gui/{app,sessions,results,visualization}.js, gui/{index.html,styles.css} + retain successful runs in browser memory with unique IDs, editable names, diff --git a/README.md b/README.md index e5b5f6b0..ce94edbd 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ seeding of interaction sites** Predict short RNA pairs or evaluate supplied structures locally with the [browser interface](https://backofenlab.github.io/IntaRNA/gui/). -Computed runs stay in separate, foldable sessions in the current tab. Name and select sessions to restore their inputs and compare results; editing or resetting parameters keeps earlier runs. +The browser accepts up to 50 records per strand, 2–5,000 nucleotides each, with longest selected query × longest selected target at most 1,000,000. Prediction defaults to 10 interactions per pair and includes opening and seed energies; evaluation selects all energy columns. +Computed runs stay in separate, foldable sessions under Results in the current tab. Name and select sessions to restore their inputs and compare results; editing or resetting parameters keeps earlier runs. Select an interaction to view it in vaRRI, or download a session’s ZIP with inputs, compiled versions, reproducible parameters and all outputs. Sessions are temporary and are cleared when the page is reloaded. See the [WebAssembly build instructions](gui/docs/step1-wasm-compilation.md) and [interface/deployment notes](gui/docs/step4-results-and-deployment.md). diff --git a/gui/app.js b/gui/app.js index d265b466..446a7f60 100644 --- a/gui/app.js +++ b/gui/app.js @@ -1,6 +1,6 @@ import { parseFasta, prepareRequest, CSV_COLUMNS } from './protocol.js'; -import { GROUPS, FIELDS, EXTRA_OUTPUTS } from './options.js'; -import { CSV_CATALOG } from './columns.js'; +import { GROUPS, FIELDS, EXTRA_OUTPUTS, UI_DEFAULTS } from './options.js'; +import { CSV_CATALOG, CSV_GROUPS, defaultColumns } from './columns.js'; import { PredictionRunner } from './runner.js'; import { downloadText } from './results.js'; import { SessionsView } from './sessions.js'; @@ -44,7 +44,7 @@ function group(key, label) { const body = element('div', '', 'group-body'); panel.append(body); byId('parameter-groups').append(panel); addTab(key, label); return body; } -addTab('sequence-input', 'Sequence input'); +addTab('sequence-input', 'Sequences'); byId('parameter-tabs').addEventListener('keydown', (event) => { if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; const tabs = [...byId('parameter-tabs').children].filter((tab) => !tab.hidden); @@ -52,34 +52,67 @@ byId('parameter-tabs').addEventListener('keydown', (event) => { const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : (current + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length; event.preventDefault(); activateGroup(tabs[next].getAttribute('aria-controls'), true); }); +// Render each requested row explicitly; labels share a grid row so wrapped +// labels do not push neighboring controls out of alignment. +function renderField(field, id = field.key) { + const wrapper = element('div', '', `field${field.type === 'checkbox' ? ' check' : ''}`); + const label = element('label', field.label); label.htmlFor = id; label.title = `--${field.key}`; + const input = element(field.type === 'select' ? 'select' : field.type === 'textarea' ? 'textarea' : 'input'); input.id = id; + if (input.tagName === 'INPUT') input.type = field.type; + if (field.type === 'select') for (const [value, name] of Object.entries(field.choices)) { const option = element('option', name); option.value = value; input.append(option); } + if (field.type === 'number') { input.min = field.min; input.max = field.max; input.step = field.step ?? 1; } + if (field.type === 'checkbox') { input.checked = field.value; wrapper.append(input, label); } + else { input.value = field.value; wrapper.append(label, input); } + if (field.hint) { const hint = element('p', field.hint, 'field-hint'); hint.id = `${id}-help`; input.setAttribute('aria-describedby', hint.id); wrapper.append(hint); } + if (field.type === 'textarea') { + input.spellcheck = false; + const upload = element('input'); upload.type = 'file'; upload.accept = '.txt,.csv,.shape,.dat'; upload.setAttribute('aria-label', `Load ${field.label} from a local text file`); + upload.dataset.for = field.key; + upload.addEventListener('change', async () => { + const file = upload.files[0]; upload.value = ''; if (!file) return; + const revision = ++inputRevision; + try { + if (file.size > 1000000) throw new Error('Choose a text file of at most 1 MB.'); + const content = await file.text(); + if (inputRevision !== revision || input.disabled) return; + input.value = content; input.dispatchEvent(new Event('input', { bubbles: true })); + } catch (error) { if (inputRevision === revision) showInputError(error); } + }); wrapper.append(upload); + } + if (field.key === 'noSeed') wrapper.hidden = true; + return wrapper; +} +function fieldRow(body, fields, suffix = '') { + const grid = element('div', '', `field-grid columns-${fields.length}`); + for (const field of fields) grid.append(renderField(field, field.key + suffix)); + body.append(grid); return grid; +} for (const definition of GROUPS) { - const body = group(definition.key, definition.label), grid = element('div', '', 'field-grid'); body.append(grid); - for (const field of definition.fields) { - const wrapper = element('div', '', `field${field.type === 'checkbox' ? ' check' : ''}`); - const label = element('label', field.label); label.htmlFor = field.key; label.title = `--${field.key}`; - const input = element(field.type === 'select' ? 'select' : field.type === 'textarea' ? 'textarea' : 'input'); input.id = field.key; - if (input.tagName === 'INPUT') input.type = field.type; - if (field.type === 'select') for (const [value, name] of Object.entries(field.choices)) { const option = element('option', name); option.value = value; input.append(option); } - if (field.type === 'number') { input.min = field.min; input.max = field.max; input.step = field.step ?? 1; } - if (field.type === 'checkbox') { input.checked = field.value; wrapper.append(input, label); } - else { input.value = field.value; wrapper.append(label, input); } - if (field.hint) { const hint = element('p', field.hint, 'field-hint'); hint.id = `${field.key}-help`; input.setAttribute('aria-describedby', hint.id); wrapper.append(hint); } - if (field.type === 'textarea') { - input.spellcheck = false; - const upload = element('input'); upload.type = 'file'; upload.accept = '.txt,.csv,.shape,.dat'; upload.setAttribute('aria-label', `Load ${field.label} from a local text file`); - upload.dataset.for = field.key; - upload.addEventListener('change', async () => { - const file = upload.files[0]; upload.value = ''; if (!file) return; - const revision = ++inputRevision; - try { - if (file.size > 1000000) throw new Error('Choose a text file of at most 1 MB.'); - const content = await file.text(); - if (inputRevision !== revision) return; - input.value = content; input.dispatchEvent(new Event('input', { bubbles: true })); - } catch (error) { if (inputRevision === revision) showInputError(error); } - }); wrapper.append(upload); + const body = group(definition.key, definition.label); + if (definition.key === 'seed-options') { + const wrapper = element('div', '', 'field seed-mode'); + const label = element('label', 'Seed constraint'); label.htmlFor = 'seed-mode'; + const select = element('select'); select.id = 'seed-mode'; + for (const [value, name] of [['none', 'Disable seed constraint'], ['explicit', 'Explicit seed'], ['identify', 'Identify seeds']]) { + const option = element('option', name); option.value = value; select.append(option); } - grid.append(wrapper); + select.value = 'identify'; wrapper.append(label, select); body.append(wrapper); + } + if (definition.rows) { + for (const row of definition.rows) fieldRow(body, row); + } else fieldRow(body, definition.fields); + if (definition.strand) { + const p = definition.strand; + const shared = FIELDS.filter((field) => ['accNoLP', 'accNoGUend'].includes(field.key)); + fieldRow(body, shared, p === 't' ? '-target' : ''); + body.append(element('p', 'These two accessibility settings apply to both strands.', 'input-help')); + const columns = element('div', '', 'accessibility-columns'); body.append(columns); + const accessibility = element('div'), shape = element('div'); columns.append(accessibility, shape); + const field = (key) => FIELDS.find((entry) => entry.key === p + key); + fieldRow(accessibility, [field('AccConstr')]); + fieldRow(accessibility, [field('AccFile')]); + fieldRow(shape, [field('Shape')]); + fieldRow(shape, [field('ShapeMethod'), field('ShapeConversion')]); } } function checkbox(container, key, labelText, description) { @@ -89,20 +122,38 @@ function checkbox(container, key, labelText, description) { } const outputBody = byId('output-options').querySelector('.group-body'); outputBody.append(element('h3', 'CSV output columns', 'subheading')); -outputBody.append(element('p', 'seq1 is the target; seq2 is the query. Select columns for both the table and CSV download. You can hide table columns after computation without changing the CSV.', 'input-help')); +outputBody.append(element('p', 'seq1 is the target; seq2 is the query. Hover over a column name for details. Select columns for the table and CSV download; table visibility can be changed after computation.', 'input-help')); const selectionTools = element('div', '', 'selection-tools'); outputBody.append(selectionTools); -const csvPicker = element('div', '', 'checkbox-grid'); outputBody.append(csvPicker); -for (const column of [...CSV_COLUMNS.split(',').map((key) => CSV_CATALOG.find((column) => column.key === key)), ...CSV_CATALOG.filter((column) => !CSV_COLUMNS.split(',').includes(column.key))]) csvInputs.push(checkbox(csvPicker, column.key, `${column.key} — ${column.description}`, column.description)); -for (const [name, selection] of [['Select all', CSV_CATALOG.map((c) => c.key)], ['CLI defaults', CSV_COLUMNS.split(',')], ['Clear selection', []]]) { +const selectColumns = (selection) => { + csvOrder = [...selection]; for (const input of csvInputs) input.checked = selection.includes(input.value); inputsChanged(); +}; +for (const [name, selection] of [['Select all', () => CSV_CATALOG.map((c) => c.key)], ['Task defaults', () => defaultColumns(getTask())], ['Clear selection', () => []]]) { const button = element('button', name); button.type = 'button'; - button.addEventListener('click', () => { csvOrder = [...selection]; for (const input of csvInputs) input.checked = selection.includes(input.value); inputsChanged(); }); selectionTools.append(button); + button.addEventListener('click', () => selectColumns(selection())); selectionTools.append(button); +} +for (const group of CSV_GROUPS) { + const section = element('fieldset', '', 'csv-group'), legend = element('legend', group.label); + section.dataset.csvGroup = group.label; + for (const [name, checked] of [['Select all', true], ['Deselect all', false]]) { + const button = element('button', name, 'text-button'); button.type = 'button'; + button.setAttribute('aria-label', `${name} ${group.label} columns`); + button.addEventListener('click', () => { + for (const input of csvInputs.filter((input) => group.keys.includes(input.value))) input.checked = checked; + inputsChanged(); + }); legend.append(button); + } + const picker = element('div', '', 'checkbox-grid csv-columns'); section.append(legend, picker); outputBody.append(section); + for (const key of group.keys) { + const column = CSV_CATALOG.find((column) => column.key === key); + csvInputs.push(checkbox(picker, key, key, column.description)); + } } outputBody.append(element('h3', 'Additional output files', 'subheading')); outputBody.append(element('p', 'Reports are computed with the same settings. ASCII, detailed and ensemble reports each run a fresh engine instance and can take extra time.', 'input-help')); const outputPicker = element('div', '', 'checkbox-grid'); outputBody.append(outputPicker); for (const output of EXTRA_OUTPUTS) outputInputs.push(checkbox(outputPicker, output.key, output.label)); -const generalBody = group('general-options', 'General'); -generalBody.append(element('p', 'Tasks run locally in one worker. Abort or Escape stops computation immediately. Process-level settings (threads, log paths, parameter-file paths and terminal help) are available in the CLI.')); +const generalBody = group('general-options', 'CLI-only'); +generalBody.append(element('p', 'Process-level settings (threads, log paths, parameter-file paths and terminal help) are available in the CLI.')); const docs = element('a', 'Full CLI parameter documentation'); docs.href = 'https://github.com/BackofenLab/IntaRNA#readme'; generalBody.append(docs); function inputsChanged(message = 'Inputs changed. Compute to update the interactions.') { @@ -111,9 +162,11 @@ function inputsChanged(message = 'Inputs changed. Compute to update the interact byId('diagnostics').hidden = true; byId('diagnostic-text').textContent = ''; status.textContent = message; } function updateLength(name) { + const warning = byId(`${name}-warning`); warning.hidden = true; warning.textContent = ''; if (!byId(name).value.trim()) { byId(`${name}-length`).textContent = 'No sequence entered'; return; } try { const parsed = parseFasta(byId(name).value, name === 'query' ? 'Query' : 'Target', getTask() === 'prediction'); + warning.textContent = parsed.warnings.join(' '); warning.hidden = !parsed.warnings.length; byId(`${name}-length`).textContent = `${parsed.records.length} record${parsed.records.length === 1 ? '' : 's'}, ${parsed.records.reduce((sum, r) => sum + r.sequence.length, 0).toLocaleString()} nucleotides`; } catch { byId(`${name}-length`).textContent = 'Check sequence format and length'; } } @@ -130,9 +183,22 @@ function updateAvailability() { } for (const field of definition.fields) { const input = byId(field.key); - input.disabled = evaluation && !!field.prediction || !evaluation && definition.key === 'helix-options' && byId('model').value !== 'B' || field.key === 'kineticScore' && byId('mode').value !== 'K'; + input.disabled = evaluation && !!field.prediction || field.key.startsWith('helix') && byId('model').value !== 'B' || field.key === 'kineticScore' && byId('mode').value !== 'K'; } } + byId('noSeed').checked = byId('seed-mode').value === 'none'; + for (const field of FIELDS.filter((field) => field.key.startsWith('seed'))) { + byId(field.key).disabled = evaluation || (field.key === 'seedTQ' ? byId('seed-mode').value !== 'explicit' : byId('seed-mode').value !== 'identify'); + } + for (const p of ['q', 't']) { + const mode = byId(`${p}Acc`).value; + for (const key of ['AccW', 'AccL', 'PfScale', 'AccConstr', 'AccFile', 'Shape', 'ShapeMethod', 'ShapeConversion']) { + byId(p + key).disabled = mode === 'N' || ['AccW', 'AccL'].includes(key) && mode !== 'C' || key === 'AccFile' && !['P', 'E'].includes(mode); + } + for (const key of ['accNoLP', 'accNoGUend']) byId(key + (p === 't' ? '-target' : '')).disabled = mode === 'N'; + } + // Upload controls follow the text area they populate, including async reads. + for (const upload of form.querySelectorAll('input[data-for]')) upload.disabled = byId(upload.dataset.for).disabled; for (const input of outputInputs) { input.disabled = evaluation && EXTRA_OUTPUTS.find((entry) => entry.key === input.value).prediction; if (input.disabled) input.checked = false; } updateLength('query'); updateLength('target'); } @@ -143,17 +209,26 @@ function resetParameters({ clearInputs = false } = {}) { for (const input of form.querySelectorAll('input[type=file]')) input.value = ''; byId('examples-menu').open = false; activateGroup('sequence-input'); } - csvOrder = CSV_COLUMNS.split(','); - for (const field of FIELDS) { const input = byId(field.key); if (field.type === 'checkbox') input.checked = field.value; else input.value = field.value; } - for (const input of csvInputs) input.checked = CSV_COLUMNS.split(',').includes(input.value); + csvOrder = defaultColumns(getTask()); + byId('seed-mode').value = 'identify'; + for (const field of FIELDS) { const input = byId(field.key); if (field.type === 'checkbox') input.checked = UI_DEFAULTS[field.key]; else input.value = UI_DEFAULTS[field.key]; } + syncAccessibilityFlags(); + for (const input of csvInputs) input.checked = csvOrder.includes(input.value); for (const input of outputInputs) input.checked = false; byId('qIdxPos0').value = 1; byId('tIdxPos0').value = 1; byId('rri').value = ''; - updateAvailability(); errorBox.hidden = true; inputsChanged(clearInputs ? 'Inputs cleared. Enter sequences or choose an example.' : 'Parameters reset to CLI defaults. Compute to update the interactions.'); + updateAvailability(); errorBox.hidden = true; inputsChanged(clearInputs ? 'Inputs cleared. Enter sequences or choose an example.' : 'Parameters reset to task defaults. Compute to update the interactions.'); } byId('task').addEventListener('change', resetParameters); byId('reset').addEventListener('click', () => resetParameters({ clearInputs: true })); +function syncAccessibilityFlags(source) { + for (const key of ['accNoLP', 'accNoGUend']) { + if (source?.id === key + '-target') byId(key).checked = source.checked; + byId(key + '-target').checked = byId(key).checked; + } +} form.addEventListener('input', (event) => { if (event.target.type === 'file') return; + syncAccessibilityFlags(event.target); updateAvailability(); errorBox.hidden = true; inputsChanged(); }); function showInputError(error) { @@ -169,6 +244,8 @@ function applyParameters(request, message = 'Parameters loaded. Compute to analy if (field.type === 'checkbox') byId(field.key).checked = value; else byId(field.key).value = value; } + byId('seed-mode').value = request.parameters.noSeed ? 'none' : request.parameters.seedTQ ? 'explicit' : 'identify'; + syncAccessibilityFlags(); csvOrder = [...request.csvColumns]; for (const input of csvInputs) input.checked = request.csvColumns.includes(input.value); for (const input of outputInputs) input.checked = request.outputs.includes(input.value); @@ -253,13 +330,24 @@ byId('examples-menu').addEventListener('keydown', (event) => { byId('abort').addEventListener('click', () => runner.abort()); dialog.addEventListener('cancel', (event) => { event.preventDefault(); runner.abort(); }); window.addEventListener('pagehide', () => runner.abort()); +function currentParameters() { + // Keep inactive edits in the form for toggling back, but exclude them from + // validation, saved JSON and native computation. Shared flags stay active + // whenever either strand uses accessibility. + return Object.fromEntries(FIELDS.map((field) => { + const input = byId(field.key); + const inactive = ['accNoLP', 'accNoGUend'].includes(field.key) + ? input.disabled && byId(field.key + '-target').disabled : input.disabled; + return [field.key, inactive ? field.value : field.type === 'checkbox' ? input.checked : input.value]; + })); +} function currentRequest() { const selected = csvInputs.filter((input) => input.checked).map((input) => input.value); const columns = [...csvOrder.filter((key) => selected.includes(key)), ...selected.filter((key) => !csvOrder.includes(key))]; return { task: getTask(), query: byId('query').value, target: byId('target').value, qIdxPos0: byId('qIdxPos0').value, tIdxPos0: byId('tIdxPos0').value, rri: byId('rri').value, - parameters: Object.fromEntries(FIELDS.map((field) => [field.key, field.type === 'checkbox' ? byId(field.key).checked : byId(field.key).value])), + parameters: currentParameters(), csvColumns: columns, outputs: outputInputs.filter((input) => input.checked && !input.disabled).map((input) => input.value), }; } @@ -268,7 +356,9 @@ form.addEventListener('submit', async (event) => { const request = currentRequest(); errorBox.hidden = true; let prepared; - try { prepared = prepareRequest(request); } catch (error) { errorBox.textContent = error.message; errorBox.hidden = false; return; } + try { + if (getTask() === 'prediction' && byId('seed-mode').value === 'explicit' && !request.parameters.seedTQ.trim()) throw new Error('Explicit seed: enter target&query dot-bar encodings.'); + prepared = prepareRequest(request); } catch (error) { errorBox.textContent = error.message; errorBox.hidden = false; return; } inputsChanged('Computing interactions…'); byId('loading-status').textContent = 'Loading the prediction engine…'; byId('inputs').disabled = true; byId('results').setAttribute('aria-busy', 'true'); dialog.showModal(); try { diff --git a/gui/columns.js b/gui/columns.js index 6b42b93a..9c096021 100644 --- a/gui/columns.js +++ b/gui/columns.js @@ -313,3 +313,23 @@ export const CSV_CATALOG = [ "numeric": true } ]; + +// Every native column appears once. Ensemble energies and their components are +// grouped with probabilities so users can see the cost/meaning of those outputs. +export const CSV_GROUPS = [ + { label: 'Energy', keys: ['E', 'ED1', 'ED2', 'E_init', 'E_loops', 'E_dangleL', 'E_dangleR', 'E_endL', 'E_endR', 'E_hybrid', 'E_norm', 'E_hybridNorm', 'E_add', 'seedE', 'seedED1', 'seedED2'] }, + { label: 'Output', keys: ['subseqDP', 'subseqDB', 'hybridDP', 'hybridDB', 'hybridDPfull', 'hybridDBfull', 'bpList'] }, + { label: 'Sequence', keys: ['id1', 'id2', 'seq1', 'seq2', 'subseq1', 'subseq2'] }, + { label: 'Interaction', keys: ['start1', 'end1', 'start2', 'end2', 'seedStart1', 'seedEnd1', 'seedStart2', 'seedEnd2'] }, + { label: 'Partition Function', keys: ['Pu1', 'Pu2', 'seedPu1', 'seedPu2', 'w', 'Etotal', 'Eall', 'Eall1', 'Eall2', 'Zall', 'Zall1', 'Zall2', 'EallTotal', 'P_E', 'RT'] }, +]; + +export function defaultColumns(task) { + const context = ['id1', 'start1', 'end1', 'id2', 'start2', 'end2', 'subseqDP', 'hybridDP', 'E']; + // Seed energies remain available in evaluation, where native output is NAN. + // List them explicitly: seedEnd1/2 are coordinates, not energy columns. + const extra = task === 'evaluation' + ? [...CSV_CATALOG.filter(({ key }) => key.startsWith('E')).map(({ key }) => key), 'seedE', 'seedED1', 'seedED2'] + : ['ED1', 'ED2', 'seedE', 'seedStart1', 'seedStart2']; + return [...new Set([...context, ...extra])]; +} diff --git a/gui/docs/json-workflow.md b/gui/docs/json-workflow.md index 342d1f24..df5f7dfb 100644 --- a/gui/docs/json-workflow.md +++ b/gui/docs/json-workflow.md @@ -1,6 +1,7 @@ # JSON settings and reproducible runs -Load and Store sit to the right of Reset parameters. Store downloads +The action bar starts with Compute/Evaluate, followed by Input Parameters: +Examples, Store, Load and Reset. Store downloads `parameter.json` containing non-default settings and embedded input text in the [native JSON format](../../doc/json-parameters.md). `_comment` records compiled tool/library versions and executable reproduction commands. It is JSON metadata, @@ -35,11 +36,11 @@ CSV output, additional files and diagnostics. Editing parameters, Load, examples Reset, invalid inputs, failed computations and cancellation preserve prior sessions. Failed or aborted computations do not create a session. -The newest session opens automatically. Use **Show results** (or **Restore -inputs** after editing), or select its output area, to restore that session’s +The newest session opens automatically under **Results**. Use **Show**, select +its header/name, or select its output area to restore that session’s computed inputs and fold the other sessions. The task, sequence text, signed indices, SHAPE/accessibility text, CSV selection/order and additional output -selection are restored together. **Hide results** folds the output. Each session +selection are restored together. **Hide** folds the output. Each session keeps its own filter, sorting and visible columns; folded vaRRI frames are unloaded and restored when their session is reopened. @@ -60,8 +61,14 @@ JSON explicitly records that version information is unavailable. ## Reset and examples Reset clears the editable sequences, interactions and uploaded files and restores -prediction defaults. Previously computed sessions remain available. Empty FASTA -fields show the short demonstration sequences as placeholders only. The Example disclosure menu sits to the left of Reset. +prediction defaults (10 interactions per pair, expanded energy/seed columns). +Evaluation defaults select all energy columns. These GUI defaults are written +explicitly in JSON when they differ from the CLI; importing existing JSON still +uses native defaults for omitted parameters. Inactive controls keep their edits +in the form for switching modes, but their values are omitted from executed and +stored settings. Previously computed sessions remain available. Empty FASTA +fields show the short demonstration sequences as placeholders only. The Examples +disclosure menu follows the Input Parameters label. Maintainers edit `gui/examples/index.json` (an array of local JSON filenames). Each listed file contains `name`, `description`, and `configuration`, where the @@ -73,9 +80,13 @@ examples show an error in the menu; they never overwrite the form. ## Local FASTA files Each strand has an Upload button and accepts a text-file drop into its textarea. -Files are read locally (at most 60 KB), validated with the same FASTA checks as +Files are read locally (at most 1 MB), validated with the same FASTA checks as manual entry, and replace only that strand after successful validation. Prediction -accepts multi-FASTA; evaluation requires one record. Invalid files leave the +accepts up to 50 FASTA records per strand, each 2–5,000 nt; evaluation requires +one record. The longest selected query × longest selected target must be at most +1,000,000. Names end at the first whitespace; ignored header descriptions produce +a warning below the corresponding field. Both FASTA fields scroll horizontally +and vertically. Invalid files leave the previous input intact. A later edit, reset, import or computation discards any pending file read, so delayed reads cannot replace newer input. @@ -117,3 +128,22 @@ pending file read, so delayed reads cannot replace newer input. `tests/data` directory for the existing CLI harness. - Static staging (including the new session module and engine digest checks), shell syntax and `git diff --check` passed. + +## PR #299 review validation (2026-10-24) + +- `npm test --prefix gui`: all 20 tests passed, including FASTA limits, header + warnings, complete column grouping and explicit browser-default JSON replay. +- `scripts/wasm/test-browser.py` passed with real WASM in Chromium and Firefox. + It covers the reordered panels, aligned controls, shared accessibility flags, + seed/accessibility/helix modes, inactive values, explicit-seed Load/Store, + group selection, FASTA scrolling/warnings and result header/name selection, + plus the existing prediction/evaluation, ZIP, abort and session checks. +- Desktop and 390 px mobile screenshots were inspected. All seven panels have + no document overflow; table contents retain their own horizontal scrolling. +- `node scripts/wasm/smoke-protocol.mjs ` passed native-reference + checks and byte-identical JSON archive replay. The unchanged engine uses the + parent branch's Emscripten 4.0.23 / ViennaRNA 2.7.2 assets and original metadata. +- A fresh GCC 14 release build passed `make tests -j2`: all nine API/CLI groups. + Existing CLI fixtures were linked into the out-of-tree `tests/data` directory. +- Static staging with engine digest validation, JavaScript/Python syntax checks, + shell syntax and `git diff --check` passed. diff --git a/gui/docs/step3-frontend-ui.md b/gui/docs/step3-frontend-ui.md index 5cc524bd..e0436b75 100644 --- a/gui/docs/step3-frontend-ui.md +++ b/gui/docs/step3-frontend-ui.md @@ -21,25 +21,26 @@ the group website. Footer and table notes use the available width, wrapping only when the viewport requires it. ```text -Group logo IntaRNA / RNA–RNA interaction analysis University logo +Group logo IntaRNA package / RNA–RNA interaction analysis University logo RNA–RNA interactions Computations stay on your device -Task: Prediction / Evaluation Load example -[ Sequence input | Query | Target | Seed | Helix | SHAPE | Energy | Output | General ] +Task: Prediction / Evaluation +[ Sequences | Seed | Output | Model & Energy | Query | Target | CLI-only ] [ Active panel: e.g. query + target, start indices, evaluation structures ] -Reset parameters Compute -Interactions / search / show-hide columns / CSV + ZIP download +Compute Input Parameters: Examples / Store / Load / Reset +Results / Show-Hide / name / ZIP download / delete +Interactions / search / show-hide columns / CSV download Selected interaction / embedded vaRRI Additional output files / diagnostics / compiled versions ``` -Every CLI group has a tab with one visible panel. Sequence input is selected +Every CLI group has a tab with one visible panel. Sequences is selected initially. Tabs expose `aria-selected`, `aria-controls` and roving focus; Left/Right move between available tabs, and Home/End select the first/last. Tab names wrap on narrow screens. Prediction-only tabs and controls are disabled/hidden during -evaluation; if the selected tab becomes unavailable, Sequence input is selected. -Changing task resets parameters to their CLI defaults while preserving sequence text; Load example also restores the -complementary pair. The native default output count is one; browser users can -request 0–100 interactions per pair. Zero enables accessibility-only output. +evaluation; if the selected tab becomes unavailable, Sequences is selected. +Changing task resets parameters to their task defaults while preserving sequence text; Load example also restores the +complementary pair. The browser defaults to 10 interactions per pair (the native default remains +one), and accepts 0–100. Zero enables accessibility-only output. ## Inputs, defaults and validation @@ -53,15 +54,29 @@ CSV uses a fixed semicolon delimiter. Process settings (threads, file/log paths, parameter files, help/version commands) are replaced by the worker lifecycle, local data inputs, downloads and displayed compiled versions. The base-pair maximization energy model is intentionally -excluded. The helix-block interaction model B is supported. +excluded. The helix-block interaction model B is supported, with helix controls at the end +of Model & Energy, enabled only for model B. Seed constraint offers Disable, +Explicit seed and Identify seeds, enabling only the relevant controls. Query and +Target include their own accessibility/SHAPE columns and synchronized copies of +the two shared accessibility flags. Ignore disables accessibility controls; +import modes disable window/span and enable the data input. Disabled settings +remain editable when re-enabled but do not enter the request. Explicit row grids +align inputs even when their labels wrap. -Prediction accepts plain sequence or up to 20 FASTA records per strand. +CSV columns are grouped as Energy, Output, Sequence, Interaction and Partition +Function, with per-group selection actions and name-only labels with description +tooltips. Prediction adds ED1, ED2, seedE, seedStart1 and seedStart2 to the native +CSV columns; evaluation selects all energy outputs. JSON records these browser +defaults explicitly so native replay retains the same output. + +Prediction accepts plain sequence or up to 50 FASTA records per strand. Evaluation requires one query and target and 1–100 predefined structures. Wrapped/lowercase sequence text and DNA T normalize to uppercase RNA U. A/C/G/U/N -are accepted. Each record is 2–2,000 nt; seed predictions additionally require +are accepted. Each record is 2–5,000 nt; seed predictions additionally require at least `seedBP` nucleotides. FASTA IDs end at the first whitespace, matching the CLI. Duplicate IDs (including collisions after -native CSV semicolon replacement) are rejected. The total selected all-vs-all -query length × target length, or sum of pairwise products, is capped at 1,000,000. +native CSV semicolon replacement) are rejected. Ignored header descriptions are reported below the input. The longest selected +query length × longest selected target length is capped at 1,000,000, including +pairwise mode; the cap is not summed over records. The per-strand start index applies to all records and defaults to one. Negative indices skip zero, matching `RnaSequence`; output coordinates remain inclusive. diff --git a/gui/examples/evaluation.json b/gui/examples/evaluation.json index b8994f18..84cab246 100644 --- a/gui/examples/evaluation.json +++ b/gui/examples/evaluation.json @@ -5,9 +5,14 @@ "schema": "IntaRNA-parameters-v1", "parameters": { "personality": "IntaRNAeval", - "query": {"text": ">query\nGGGAGGAGGG\n"}, - "target": {"text": ">target\nCCCUCCUCCC\n"}, - "rri": "1||||||||||&1||||||||||" + "query": { + "text": ">query\nGGGAGGAGGG\n" + }, + "target": { + "text": ">target\nCCCUCCUCCC\n" + }, + "rri": "1||||||||||&1||||||||||", + "outCsvCols": "id1,start1,end1,id2,start2,end2,subseqDP,hybridDP,E,Etotal,ED1,ED2,E_init,E_loops,E_dangleL,E_dangleR,E_endL,E_endR,E_hybrid,E_norm,E_hybridNorm,E_add,Eall,Eall1,Eall2,EallTotal,seedE,seedED1,seedED2" } } } diff --git a/gui/examples/prediction.json b/gui/examples/prediction.json index 2b834042..bd76029a 100644 --- a/gui/examples/prediction.json +++ b/gui/examples/prediction.json @@ -4,8 +4,14 @@ "configuration": { "schema": "IntaRNA-parameters-v1", "parameters": { - "query": {"text": ">query\nGGGAGGAGGG\n"}, - "target": {"text": ">target\nCCCUCCUCCC\n"} + "query": { + "text": ">query\nGGGAGGAGGG\n" + }, + "target": { + "text": ">target\nCCCUCCUCCC\n" + }, + "outNumber": 10, + "outCsvCols": "id1,start1,end1,id2,start2,end2,subseqDP,hybridDP,E,ED1,ED2,seedE,seedStart1,seedStart2" } } } diff --git a/gui/index.html b/gui/index.html index 679c15b0..a77b74e7 100644 --- a/gui/index.html +++ b/gui/index.html @@ -14,7 +14,7 @@
@@ -38,33 +38,33 @@
5′ to 3′
or drop in the field
- -
No sequence entered
+ +
No sequence entered
5′ to 3′
or drop in the field
- -
No sequence entered
+ +
No sequence entered
-

Plain sequence or multi-FASTA (prediction only), A/C/G/U/T/N. Up to 20 records per field and 2–2,000 nt per record; total query × target length across selected pairs ≤ 1,000,000. Seed predictions also require at least as many nucleotides as seed base pairs. Signed start indices apply to every record on that strand; negative indexing skips zero.

+

Plain sequence or multi-FASTA (prediction only), A/C/G/U/T/N. Up to 50 records per field and 2–5,000 nt per record; longest selected query × longest selected target ≤ 1,000,000. FASTA names stop at the first whitespace. Seed predictions also require at least as many nucleotides as seed base pairs. Signed start indices apply to every record on that strand; negative indexing skips zero.

-
Example

Loading examples…

+
Input Parameters:
Examples

Loading examples…

-

Sessions

Enter your sequences or choose an example, then compute.

+

Results

Enter your sequences or choose an example, then compute.

Completed runs stay in this tab until deleted or the page is reloaded. Select a session to restore its inputs; download its ZIP to keep a copy.