Files
prautoblogger/assets/js/gen-history.js
peptiderepo 16fb9fdc5d feat: Pipeline Settings M4 — generation history + full per-step input/output (#174, v0.26.0)
Paginated generations list + AJAX stage-I/O drill-down (full input + output per step); entry into the existing dossier; stage-list-driven; read-only; no secret leak. Sweeps M3 P2s. QA APPROVE-WITH-P2 (baae383 carried to b05c873); CI green.

Agent-Session: pipeline-settings-m4-merge
2026-06-23 15:13:53 +07:00

205 lines
6.6 KiB
JavaScript

/**
* Generation History page — Stage I/O drill-down (M4).
*
* Handles the "Stage I/O" toggle button on each run row:
* 1. On first click: POST to prautoblogger_gen_run_io, receive stage
* data, render the per-stage accordion (input_system, input_user,
* output).
* 2. On subsequent clicks: toggle the row's visibility (no re-fetch).
*
* SECURITY: Renders all text via textContent — NEVER innerHTML for
* prompt/response content. The nonce is passed via wp_localize_script
* as prabGenHistory.nonce (not a data-* attribute).
*
* @see ajax/class-gen-run-io-handler.php -- AJAX endpoint.
* @see admin/class-gen-history-page.php -- Localises prabGenHistory config.
*/
( function () {
'use strict';
/** @type {{ ajaxUrl: string, action: string, nonce: string, strings: Object }} */
var cfg = window.prabGenHistory || {};
/**
* Render a single stage's I/O block.
*
* @param {Object} stage Stage data from AJAX response.
* @returns {HTMLElement}
*/
function renderStage( stage ) {
var wrap = document.createElement( 'div' );
wrap.className = 'prab-io-stage';
// Header.
var header = document.createElement( 'div' );
header.className = 'prab-io-stage__header';
var label = document.createElement( 'span' );
label.className = 'prab-io-stage__label';
label.textContent = stage.label || stage.stage;
header.appendChild( label );
var meta = document.createElement( 'span' );
meta.className = 'prab-io-stage__meta';
var costStr = stage.estimated_cost ? '$' + stage.estimated_cost.toFixed( 6 ) : '';
var tokStr = stage.prompt_tokens
? stage.prompt_tokens + ' in / ' + stage.completion_tokens + ' out tokens'
: '';
var metaParts = [ stage.model, tokStr, costStr ].filter( Boolean );
meta.textContent = metaParts.join( ' · ' );
header.appendChild( meta );
var statusChip = document.createElement( 'span' );
statusChip.className = 'prab-chip prab-chip--' + ( stage.response_status || '' );
statusChip.textContent = stage.response_status || '';
header.appendChild( statusChip );
wrap.appendChild( header );
// Body: system input, user input, output.
var body = document.createElement( 'div' );
body.className = 'prab-io-stage__body';
body.appendChild( renderSection(
/* translators: label for system prompt section */
cfg.strings && cfg.strings.systemLabel || 'System Prompt (Input)',
stage.input_system,
! stage.input_system ? ( cfg.strings.noInput || '[No input stored]' ) : null
) );
body.appendChild( renderSection(
/* translators: label for user instruction section */
cfg.strings && cfg.strings.userLabel || 'Assembled Instruction (Input)',
stage.input_user,
! stage.input_user ? ( cfg.strings.noInput || '[No input stored]' ) : null
) );
var outputMissing = null;
if ( stage.output_pruned ) {
outputMissing = cfg.strings.pruned || '[Output pruned by retention policy]';
} else if ( stage.output === null ) {
outputMissing = cfg.strings.noOutput || '[No output stored for this stage]';
}
body.appendChild( renderSection(
cfg.strings && cfg.strings.outputLabel || 'Model Response (Output)',
stage.output,
outputMissing
) );
if ( stage.error_message ) {
body.appendChild( renderSection( 'Error', stage.error_message, null ) );
}
wrap.appendChild( body );
return wrap;
}
/**
* Render a labelled text section.
*
* @param {string} title Section heading.
* @param {string|null} text Content text (null when missing).
* @param {string|null} missingMsg Message to show when text is null.
* @returns {HTMLElement}
*/
function renderSection( title, text, missingMsg ) {
var section = document.createElement( 'div' );
section.className = 'prab-io-section';
var heading = document.createElement( 'div' );
heading.className = 'prab-io-section__title';
heading.textContent = title;
section.appendChild( heading );
var content = document.createElement( 'pre' );
if ( text ) {
content.className = 'prab-io-section__text';
// textContent — never innerHTML — for security.
content.textContent = text;
} else {
content.className = 'prab-io-section__text prab-io-section__text--missing';
content.textContent = missingMsg || '';
}
section.appendChild( content );
return section;
}
/**
* Fetch and render stage I/O for a run.
*
* @param {string} runId Pipeline run UUID.
* @param {HTMLElement} panel The panel container element to populate.
*/
function fetchRunIO( runId, panel ) {
var fd = new FormData();
fd.append( 'action', cfg.action || 'prautoblogger_gen_run_io' );
fd.append( 'nonce', cfg.nonce || '' );
fd.append( 'run_id', runId );
fetch( cfg.ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' } )
.then( function ( r ) { return r.json(); } )
.then( function ( resp ) {
panel.innerHTML = '';
if ( ! resp.success ) {
var err = document.createElement( 'p' );
err.textContent = ( resp.data && resp.data.message ) || cfg.strings.error;
panel.appendChild( err );
return;
}
var stages = resp.data.stages || [];
if ( 0 === stages.length ) {
var none = document.createElement( 'p' );
none.textContent = cfg.strings.noStages || 'No stage entries found.';
panel.appendChild( none );
return;
}
stages.forEach( function ( s ) {
panel.appendChild( renderStage( s ) );
} );
// If dossier URL available, append a link.
var run = resp.data.run || {};
if ( run.dossier_url ) {
var link = document.createElement( 'p' );
link.style.marginTop = '12px';
var a = document.createElement( 'a' );
a.href = run.dossier_url;
a.textContent = 'Open full Article Dossier →';
link.appendChild( a );
panel.appendChild( link );
}
} )
.catch( function () {
panel.innerHTML = '';
var err = document.createElement( 'p' );
err.textContent = cfg.strings.error || 'Request failed.';
panel.appendChild( err );
} );
}
/** Wire "Stage I/O" toggle buttons on DOMContentLoaded. */
document.addEventListener( 'DOMContentLoaded', function () {
var buttons = document.querySelectorAll( '.prab-gen-history__io-toggle' );
buttons.forEach( function ( btn ) {
var runId = btn.dataset.runId;
var ioRow = document.getElementById( 'prab-io-' + runId );
if ( ! ioRow ) {
return;
}
var panel = ioRow.querySelector( '.prab-gen-history__io-panel' );
var loaded = false;
btn.addEventListener( 'click', function () {
if ( ioRow.hidden ) {
ioRow.hidden = false;
if ( ! loaded ) {
loaded = true;
fetchRunIO( runId, panel );
}
} else {
ioRow.hidden = true;
}
} );
} );
} );
} )();