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 (baae383carried tob05c873); CI green. Agent-Session: pipeline-settings-m4-merge
205 lines
6.6 KiB
JavaScript
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;
|
|
}
|
|
} );
|
|
} );
|
|
} );
|
|
} )();
|