feat: Pipeline Settings M3 — assembled-instructions preview + prompt version history/diff (#173, v0.25.0)
Read-only assembled-instructions preview (last-run rendered prompt) with Template-vs-Preview guardrail; per-prompt version history + LCS diff. Stage-list-driven. QA APPROVE df92ed3; CI green. Agent-Session: pipeline-settings-m3-merge
This commit is contained in:
@@ -179,3 +179,201 @@
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* ── M3: Template / Preview toggle ────────────────────────────────────────── */
|
||||
.pab-tp-toggle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.pab-tp-toggle {
|
||||
display: flex;
|
||||
border: 1px solid #d0d5dd;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pab-tp-btn {
|
||||
padding: 6px 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #646970;
|
||||
background: #fff;
|
||||
border: none;
|
||||
border-right: 1px solid #d0d5dd;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background .12s, color .12s;
|
||||
}
|
||||
.pab-tp-btn:last-child { border-right: none; }
|
||||
.pab-tp-btn--active { background: #1B8A92; color: #fff; }
|
||||
.pab-tp-btn:not(.pab-tp-btn--active):hover { background: #f5f9fa; color: #1d2327; }
|
||||
.pab-tp-hint { font-size: 12px; color: #646970; }
|
||||
|
||||
/* ── M3: Prompt editor toolbar ─────────────────────────────────────────────── */
|
||||
.pab-prompt-editor-wrap { margin-bottom: 8px; }
|
||||
.pab-prompt-editor-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f7f9fa;
|
||||
border: 1px solid #d0d5dd;
|
||||
border-bottom: none;
|
||||
border-radius: 4px 4px 0 0;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
.pab-prompt-editor-title {
|
||||
font-size: 11px;
|
||||
font-family: 'IBM Plex Mono', 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: #646970;
|
||||
}
|
||||
.pab-prompt-form .pab-prompt-editor { border-radius: 0 0 4px 4px; }
|
||||
|
||||
/* ── M3: Preview block ─────────────────────────────────────────────────────── */
|
||||
.pab-preview-block {
|
||||
border: 1px solid #d0d5dd;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #f7f9fa;
|
||||
max-width: 820px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.pab-preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 14px;
|
||||
background: #f0f4f5;
|
||||
border-bottom: 1px solid #d0d5dd;
|
||||
}
|
||||
.pab-preview-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #646970;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
.pab-preview-readonly-badge {
|
||||
font-size: 11px;
|
||||
font-family: 'IBM Plex Mono', 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: #646970;
|
||||
background: #fff;
|
||||
border: 1px solid #d0d5dd;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.pab-preview-source-note {
|
||||
font-size: 11px;
|
||||
color: #646970;
|
||||
margin-left: auto;
|
||||
}
|
||||
.pab-preview-body {
|
||||
padding: 14px;
|
||||
font-family: 'IBM Plex Mono', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
color: #3d3d3d;
|
||||
background: #f7f9fa;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
margin: 0;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.pab-preview-loading { color: #9ca3af; font-style: italic; }
|
||||
.pab-preview-notice {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
background: #fffbeb;
|
||||
border-top: 1px solid #fde68a;
|
||||
font-size: 12px;
|
||||
color: #78350f;
|
||||
}
|
||||
.pab-preview-notice .dashicons { font-size: 14px; flex-shrink: 0; margin-top: 1px; color: #92400e; }
|
||||
|
||||
/* ── M3: Version history accordion ─────────────────────────────────────────── */
|
||||
.pab-history-accordion { margin-top: 12px; }
|
||||
.pab-history-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #646970;
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
font-family: inherit;
|
||||
}
|
||||
.pab-history-trigger:hover { color: #1B8A92; }
|
||||
.pab-history-arrow { font-size: 14px !important; transition: transform .15s; }
|
||||
.pab-history-body {
|
||||
margin-top: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.pab-version-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 14px;
|
||||
border-bottom: 1px solid #f0f0f1;
|
||||
font-size: 12px;
|
||||
}
|
||||
.pab-version-row:last-child { border-bottom: none; }
|
||||
.pab-version-num {
|
||||
font-family: 'IBM Plex Mono', 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: #1B8A92;
|
||||
min-width: 28px;
|
||||
}
|
||||
.pab-version-author { color: #1d2327; font-weight: 500; }
|
||||
.pab-version-date { font-family: 'IBM Plex Mono', 'Courier New', monospace; font-size: 11px; color: #646970; }
|
||||
.pab-version-current { margin-left: 4px; }
|
||||
|
||||
/* ── M3: Diff panel ─────────────────────────────────────────────────────────── */
|
||||
.pab-diff-panel {
|
||||
margin-top: 12px;
|
||||
background: #f7f9fa;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
max-width: 820px;
|
||||
}
|
||||
.pab-diff-header {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #1d2327;
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background: #f0f4f5;
|
||||
font-family: 'IBM Plex Mono', 'Courier New', monospace;
|
||||
}
|
||||
.pab-diff-block {
|
||||
font-family: 'IBM Plex Mono', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
padding: 4px 0;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.pab-diff-line {
|
||||
display: block;
|
||||
padding: 1px 14px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.pab-diff-line--added { background: #ecfdf5; color: #065f46; }
|
||||
.pab-diff-line--removed { background: #fef2f2; color: #9b1c1c; }
|
||||
.pab-diff-line--context { color: #3d3d3d; }
|
||||
.pab-diff-line--omitted { color: #9ca3af; font-style: italic; }
|
||||
.pab-diff-close {
|
||||
display: block;
|
||||
margin: 8px 14px 10px;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,208 @@
|
||||
/**
|
||||
* Pipeline Settings page — step rail and prompt editor interactions.
|
||||
* Pipeline Settings page — M3: Template/Preview toggle, version history accordion,
|
||||
* and inline diff panel.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Confirm before resetting a prompt to default (redundant safety; the
|
||||
* PHP onclick already handles it, but JS enhances UX).
|
||||
* M3 Responsibilities:
|
||||
* 1. Template / Preview toggle per prompt panel:
|
||||
* - "Template" tab: shows the editable textarea (default).
|
||||
* - "Preview assembled instructions" tab: fetches via AJAX and shows the
|
||||
* fully-rendered prompt text. Read-only; no save path exists in JS or PHP.
|
||||
* The two states are mutually exclusive; pressing "Template" never sends
|
||||
* any network request.
|
||||
* 2. Version history accordion: toggle show/hide per prompt key.
|
||||
* 3. Inline diff panel: on "Diff" button click, fetch the computed diff from
|
||||
* the server and render added/removed/context lines with colour coding.
|
||||
* "Close diff" hides the panel.
|
||||
*
|
||||
* Note: model selection is handled entirely by model-picker.js, which writes
|
||||
* the chosen model id directly to the hidden input named after the option
|
||||
* (e.g. name="prautoblogger_research_model"). No event bridge is needed here —
|
||||
* the form submits that value on the standard POST to handle_model_save().
|
||||
* Security notes:
|
||||
* - Preview and diff bodies are returned pre-escaped (esc_html'd) by PHP.
|
||||
* They are inserted via .text() / textContent, never innerHTML, so no
|
||||
* XSS vector exists even if the server escaping were to fail.
|
||||
* - Nonces come from the prabPipeline JS object (localized in enqueue_assets).
|
||||
*
|
||||
* @see admin/class-pipeline-settings-save-handler.php — Reads $_POST[$option_name].
|
||||
* @see assets/js/model-picker.js — Writes to the hidden input by DOM id.
|
||||
* Note: model selection is handled entirely by model-picker.js; no bridge needed.
|
||||
*
|
||||
* @see admin/class-pipeline-settings-page.php -- Localizes prabPipeline.
|
||||
* @see ajax/class-pipeline-preview-handler.php -- Preview AJAX endpoint.
|
||||
* @see ajax/class-pipeline-history-handler.php -- History/diff AJAX endpoint.
|
||||
* @see assets/js/model-picker.js -- Model picker (separate).
|
||||
*/
|
||||
/* global jQuery */
|
||||
/* global jQuery, prabPipeline */
|
||||
( function ( $ ) {
|
||||
'use strict';
|
||||
|
||||
$( function () {
|
||||
// Future UX hooks (e.g. unsaved-changes warning) go here.
|
||||
/**
|
||||
* Safely read the localized config, falling back to empty strings so
|
||||
* the page never fatals when the object is missing (e.g. on other pages).
|
||||
*
|
||||
* @type {{ ajaxUrl: string, previewAction: string, previewNonce: string,
|
||||
* historyAction: string, historyNonce: string,
|
||||
* diffAction: string, diffNonce: string }}
|
||||
*/
|
||||
var cfg = ( typeof prabPipeline !== 'undefined' ) ? prabPipeline : {};
|
||||
|
||||
// ── 1. Template / Preview toggle ──────────────────────────────────────────
|
||||
|
||||
$( document ).on( 'click', '.pab-tp-btn', function () {
|
||||
var $btn = $( this );
|
||||
var $toggle = $btn.closest( '.pab-tp-toggle' );
|
||||
var mode = $btn.data( 'mode' );
|
||||
|
||||
// Update button states.
|
||||
$toggle.find( '.pab-tp-btn' ).each( function () {
|
||||
$( this ).toggleClass( 'pab-tp-btn--active', $( this ).data( 'mode' ) === mode );
|
||||
$( this ).attr( 'aria-pressed', $( this ).data( 'mode' ) === mode ? 'true' : 'false' );
|
||||
} );
|
||||
|
||||
var templateId = $toggle.data( 'template-id' );
|
||||
var previewId = $toggle.data( 'preview-id' );
|
||||
var hintId = $toggle.data( 'toggle-hint-id' );
|
||||
var $template = $( '#' + templateId );
|
||||
var $preview = $( '#' + previewId );
|
||||
var $hint = $( '#' + hintId );
|
||||
|
||||
if ( 'template' === mode ) {
|
||||
$template.show();
|
||||
$preview.hide();
|
||||
if ( $hint.length ) {
|
||||
$hint.text( $hint.data( 'template-hint' ) || prabTrans( 'Editing the template affects all future runs.' ) );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Preview mode: show panel immediately, then fetch.
|
||||
$template.hide();
|
||||
$preview.show();
|
||||
if ( $hint.length ) {
|
||||
$hint.text( prabTrans( 'Preview only — not editable.' ) );
|
||||
}
|
||||
|
||||
var promptKey = $toggle.data( 'prompt-key' );
|
||||
var $body = $preview.find( '.js-preview-body' );
|
||||
var $source = $preview.find( '.js-preview-source' );
|
||||
|
||||
// Only fetch once per page load; use data-loaded flag.
|
||||
if ( $preview.data( 'loaded' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$body.html( '<span class="pab-preview-loading">…</span>' );
|
||||
|
||||
$.post(
|
||||
( cfg.ajaxUrl || '' ),
|
||||
{
|
||||
action: cfg.previewAction || '',
|
||||
nonce: cfg.previewNonce || '',
|
||||
prompt_key: promptKey
|
||||
},
|
||||
function ( response ) {
|
||||
if ( response && response.success ) {
|
||||
var d = response.data;
|
||||
// Insert pre-escaped text via textContent (double-safe).
|
||||
$body[ 0 ].textContent = d.preview || '';
|
||||
|
||||
if ( d.from_run ) {
|
||||
var sourceNote = d.run_post
|
||||
? d.run_date + ' · ' + d.run_post
|
||||
: d.run_date;
|
||||
$source.text( prabTrans( 'Tokens from last run · ' ) + sourceNote );
|
||||
} else {
|
||||
$source.text( prabTrans( 'Sample render — no run found yet' ) );
|
||||
}
|
||||
$preview.data( 'loaded', true );
|
||||
} else {
|
||||
$body[ 0 ].textContent = prabTrans( 'Could not load preview.' );
|
||||
}
|
||||
},
|
||||
'json'
|
||||
).fail( function () {
|
||||
$body[ 0 ].textContent = prabTrans( 'Network error loading preview.' );
|
||||
} );
|
||||
} );
|
||||
|
||||
// ── 2. Version history accordion ──────────────────────────────────────────
|
||||
|
||||
$( document ).on( 'click', '.pab-history-trigger', function () {
|
||||
var $trigger = $( this );
|
||||
var bodyId = $trigger.attr( 'aria-controls' );
|
||||
var $body = $( '#' + bodyId );
|
||||
var expanded = $trigger.attr( 'aria-expanded' ) === 'true';
|
||||
|
||||
$trigger.attr( 'aria-expanded', expanded ? 'false' : 'true' );
|
||||
$trigger.find( '.pab-history-arrow' )
|
||||
.toggleClass( 'dashicons-arrow-down-alt2', expanded )
|
||||
.toggleClass( 'dashicons-arrow-up-alt2', ! expanded );
|
||||
$body.toggle( ! expanded );
|
||||
} );
|
||||
|
||||
// ── 3. Diff button ────────────────────────────────────────────────────────
|
||||
|
||||
$( document ).on( 'click', '.pab-diff-btn', function () {
|
||||
var $btn = $( this );
|
||||
var promptKey = $btn.data( 'prompt-key' );
|
||||
var versionA = $btn.data( 'version-a' );
|
||||
var versionB = $btn.data( 'version-b' );
|
||||
var diffId = $btn.data( 'diff-target' );
|
||||
var $panel = $( '#' + diffId );
|
||||
var $header = $panel.find( '.js-diff-header' );
|
||||
var $lines = $panel.find( '.js-diff-lines' );
|
||||
|
||||
$panel.show();
|
||||
$header.text( prabTrans( 'Loading diff…' ) );
|
||||
$lines.empty();
|
||||
|
||||
$.post(
|
||||
( cfg.ajaxUrl || '' ),
|
||||
{
|
||||
action: cfg.diffAction || '',
|
||||
nonce: cfg.diffNonce || '',
|
||||
prompt_key: promptKey,
|
||||
version_a: versionA,
|
||||
version_b: versionB
|
||||
},
|
||||
function ( response ) {
|
||||
if ( response && response.success ) {
|
||||
var d = response.data;
|
||||
$header.text( d.header || '' );
|
||||
$lines.empty();
|
||||
if ( d.lines && d.lines.length ) {
|
||||
$.each( d.lines, function ( i, line ) {
|
||||
var $line = $( '<div>' ).addClass( 'pab-diff-line pab-diff-line--' + line.type );
|
||||
// line.text is pre-escaped server-side; use textContent.
|
||||
$line[ 0 ].textContent = line.text;
|
||||
$lines.append( $line );
|
||||
} );
|
||||
} else {
|
||||
$lines.text( prabTrans( 'No differences found.' ) );
|
||||
}
|
||||
} else {
|
||||
$header.text( prabTrans( 'Failed to load diff.' ) );
|
||||
}
|
||||
},
|
||||
'json'
|
||||
).fail( function () {
|
||||
$header.text( prabTrans( 'Network error loading diff.' ) );
|
||||
} );
|
||||
} );
|
||||
|
||||
// ── 4. Close diff ─────────────────────────────────────────────────────────
|
||||
|
||||
$( document ).on( 'click', '.pab-diff-close', function () {
|
||||
var diffId = $( this ).data( 'diff-id' );
|
||||
$( '#' + diffId ).hide();
|
||||
} );
|
||||
|
||||
// ── Helper: minimal i18n pass-through (strings are in PHP-rendered HTML) ──
|
||||
|
||||
/**
|
||||
* Thin translation shim. Since strings originate in PHP (already translated
|
||||
* + escaped), this is just a pass-through so JS keeps a single string origin.
|
||||
*
|
||||
* @param {string} s
|
||||
* @returns {string}
|
||||
*/
|
||||
function prabTrans( s ) {
|
||||
return s;
|
||||
}
|
||||
|
||||
}( jQuery ) );
|
||||
|
||||
Reference in New Issue
Block a user