|
|
|
|
@@ -1,9 +1,10 @@
|
|
|
|
|
/**
|
|
|
|
|
* Model picker popup for PRAutoBlogger admin settings.
|
|
|
|
|
*
|
|
|
|
|
* Fetches the OpenRouter model registry via AJAX, then renders a searchable
|
|
|
|
|
* popup overlay when the user clicks a model-select trigger button. Shows
|
|
|
|
|
* name, provider, input/output cost, and context window per model.
|
|
|
|
|
* Fetches the OpenRouter model registry via AJAX, then renders a searchable,
|
|
|
|
|
* sortable popup overlay when the user clicks a model-select trigger button.
|
|
|
|
|
* Columns: Model, Price (in / out per 1M tokens), Context window.
|
|
|
|
|
* Each column header is clickable to sort ascending/descending.
|
|
|
|
|
*
|
|
|
|
|
* @see admin/class-admin-page.php — Renders model_select fields and localizes config.
|
|
|
|
|
* @see services/class-open-router-model-registry.php — Server-side registry source.
|
|
|
|
|
@@ -11,54 +12,57 @@
|
|
|
|
|
(function ($) {
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
var config = window.prautobloggerAdmin || {};
|
|
|
|
|
var cachedModels = null;
|
|
|
|
|
var config = window.prautobloggerAdmin || {};
|
|
|
|
|
var cachedModels = null;
|
|
|
|
|
var fetchInProgress = null;
|
|
|
|
|
|
|
|
|
|
// Persists across open/close so the user's preferred sort is remembered.
|
|
|
|
|
var sortState = { col: 'name', dir: 'asc' };
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetch models from the server (cached after first call).
|
|
|
|
|
*
|
|
|
|
|
* @param {string} capability Filter capability (e.g. 'text→text').
|
|
|
|
|
* @return {jQuery.Deferred} Resolves with model array.
|
|
|
|
|
*/
|
|
|
|
|
function fetchModels(capability) {
|
|
|
|
|
function fetchModels( capability ) {
|
|
|
|
|
// Image models are embedded in the page config — no AJAX needed.
|
|
|
|
|
if (capability === 'image_generation') {
|
|
|
|
|
if ( capability === 'image_generation' ) {
|
|
|
|
|
var imgModels = config.imageModels || [];
|
|
|
|
|
return $.Deferred().resolve(imgModels);
|
|
|
|
|
return $.Deferred().resolve( imgModels );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cachedModels) {
|
|
|
|
|
return $.Deferred().resolve(filterModels(cachedModels, capability));
|
|
|
|
|
if ( cachedModels ) {
|
|
|
|
|
return $.Deferred().resolve( filterModels( cachedModels, capability ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fetchInProgress) {
|
|
|
|
|
return fetchInProgress.then(function () {
|
|
|
|
|
return filterModels(cachedModels, capability);
|
|
|
|
|
});
|
|
|
|
|
if ( fetchInProgress ) {
|
|
|
|
|
return fetchInProgress.then( function () {
|
|
|
|
|
return filterModels( cachedModels, capability );
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fetchInProgress = $.ajax({
|
|
|
|
|
url: config.ajaxUrl,
|
|
|
|
|
fetchInProgress = $.ajax( {
|
|
|
|
|
url: config.ajaxUrl,
|
|
|
|
|
method: 'POST',
|
|
|
|
|
data: {
|
|
|
|
|
action: 'prautoblogger_get_models',
|
|
|
|
|
nonce: config.modelsNonce
|
|
|
|
|
nonce: config.modelsNonce
|
|
|
|
|
},
|
|
|
|
|
timeout: 15000
|
|
|
|
|
}).then(function (response) {
|
|
|
|
|
} ).then( function ( response ) {
|
|
|
|
|
fetchInProgress = null;
|
|
|
|
|
if (response.success && Array.isArray(response.data)) {
|
|
|
|
|
if ( response.success && Array.isArray( response.data ) ) {
|
|
|
|
|
cachedModels = response.data;
|
|
|
|
|
return filterModels(cachedModels, capability);
|
|
|
|
|
return filterModels( cachedModels, capability );
|
|
|
|
|
}
|
|
|
|
|
cachedModels = [];
|
|
|
|
|
return [];
|
|
|
|
|
}, function () {
|
|
|
|
|
fetchInProgress = null;
|
|
|
|
|
cachedModels = [];
|
|
|
|
|
cachedModels = [];
|
|
|
|
|
return [];
|
|
|
|
|
});
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
return fetchInProgress;
|
|
|
|
|
}
|
|
|
|
|
@@ -70,11 +74,48 @@
|
|
|
|
|
* @param {string} capability Capability string to match.
|
|
|
|
|
* @return {Array} Filtered models.
|
|
|
|
|
*/
|
|
|
|
|
function filterModels(models, capability) {
|
|
|
|
|
if (!capability) return models;
|
|
|
|
|
return models.filter(function (m) {
|
|
|
|
|
return m.capabilities && m.capabilities.indexOf(capability) !== -1;
|
|
|
|
|
});
|
|
|
|
|
function filterModels( models, capability ) {
|
|
|
|
|
if ( ! capability ) return models;
|
|
|
|
|
return models.filter( function ( m ) {
|
|
|
|
|
return m.capabilities && m.capabilities.indexOf( capability ) !== -1;
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sort a model array according to the current sortState.
|
|
|
|
|
* Deprecated models always sink to the bottom regardless of column.
|
|
|
|
|
*
|
|
|
|
|
* @param {Array} arr Model array to sort (not mutated — returns a new array).
|
|
|
|
|
* @return {Array} Sorted copy.
|
|
|
|
|
*/
|
|
|
|
|
function applySort( arr ) {
|
|
|
|
|
var col = sortState.col;
|
|
|
|
|
var dir = sortState.dir;
|
|
|
|
|
|
|
|
|
|
return arr.slice().sort( function ( a, b ) {
|
|
|
|
|
// Deprecated always last.
|
|
|
|
|
if ( a.deprecated !== b.deprecated ) {
|
|
|
|
|
return a.deprecated ? 1 : -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var valA, valB;
|
|
|
|
|
|
|
|
|
|
if ( col === 'price' ) {
|
|
|
|
|
valA = a.input_price_per_m || 0;
|
|
|
|
|
valB = b.input_price_per_m || 0;
|
|
|
|
|
} else if ( col === 'context' ) {
|
|
|
|
|
valA = a.context_length || 0;
|
|
|
|
|
valB = b.context_length || 0;
|
|
|
|
|
} else {
|
|
|
|
|
// Default: sort by model name.
|
|
|
|
|
valA = ( a.name || '' ).toLowerCase();
|
|
|
|
|
valB = ( b.name || '' ).toLowerCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( valA < valB ) return dir === 'asc' ? -1 : 1;
|
|
|
|
|
if ( valA > valB ) return dir === 'asc' ? 1 : -1;
|
|
|
|
|
return 0;
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -83,11 +124,11 @@
|
|
|
|
|
* @param {number} price Price per 1M tokens.
|
|
|
|
|
* @return {string} Formatted string like "$0.25" or "Free".
|
|
|
|
|
*/
|
|
|
|
|
function formatPrice(price) {
|
|
|
|
|
if (!price || price <= 0) return 'Free';
|
|
|
|
|
if (price < 0.01) return '$' + price.toFixed(4);
|
|
|
|
|
if (price < 1) return '$' + price.toFixed(2);
|
|
|
|
|
return '$' + price.toFixed(2);
|
|
|
|
|
function formatPrice( price ) {
|
|
|
|
|
if ( ! price || price <= 0 ) return 'Free';
|
|
|
|
|
if ( price < 0.01 ) return '$' + price.toFixed( 4 );
|
|
|
|
|
if ( price < 1 ) return '$' + price.toFixed( 2 );
|
|
|
|
|
return '$' + price.toFixed( 2 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -96,10 +137,10 @@
|
|
|
|
|
* @param {number} ctx Context length in tokens.
|
|
|
|
|
* @return {string} Formatted string like "128K" or "1M".
|
|
|
|
|
*/
|
|
|
|
|
function formatContext(ctx) {
|
|
|
|
|
if (!ctx) return '—';
|
|
|
|
|
if (ctx >= 1000000) return (ctx / 1000000).toFixed(0) + 'M';
|
|
|
|
|
if (ctx >= 1000) return Math.round(ctx / 1000) + 'K';
|
|
|
|
|
function formatContext( ctx ) {
|
|
|
|
|
if ( ! ctx ) return '—';
|
|
|
|
|
if ( ctx >= 1000000 ) return ( ctx / 1000000 ).toFixed( 0 ) + 'M';
|
|
|
|
|
if ( ctx >= 1000 ) return Math.round( ctx / 1000 ) + 'K';
|
|
|
|
|
return ctx.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -109,27 +150,29 @@
|
|
|
|
|
* @param {Object} model Normalized model record.
|
|
|
|
|
* @return {string} HTML string.
|
|
|
|
|
*/
|
|
|
|
|
function renderModelRow(model) {
|
|
|
|
|
var deprecated = model.deprecated ? ' ab-mp-deprecated' : '';
|
|
|
|
|
function renderModelRow( model ) {
|
|
|
|
|
var deprecated = model.deprecated ? ' ab-mp-deprecated' : '';
|
|
|
|
|
var isImageModel = model.cost_per_image !== undefined && model.cost_per_image > 0;
|
|
|
|
|
var priceHtml, priceTitle, metaRight;
|
|
|
|
|
|
|
|
|
|
if (isImageModel) {
|
|
|
|
|
priceHtml = '$' + model.cost_per_image.toFixed(4) + '/image';
|
|
|
|
|
if ( isImageModel ) {
|
|
|
|
|
priceHtml = '$' + model.cost_per_image.toFixed( 4 ) + '/image';
|
|
|
|
|
priceTitle = 'Cost per generated image';
|
|
|
|
|
metaRight = model.description ? '<span class="ab-mp-ctx" title="Details">' + escHtml(model.description) + '</span>' : '';
|
|
|
|
|
metaRight = model.description
|
|
|
|
|
? '<span class="ab-mp-ctx" title="Details">' + escHtml( model.description ) + '</span>'
|
|
|
|
|
: '';
|
|
|
|
|
} else {
|
|
|
|
|
priceHtml = formatPrice(model.input_price_per_m) + ' / ' + formatPrice(model.output_price_per_m);
|
|
|
|
|
priceHtml = formatPrice( model.input_price_per_m ) + ' / ' + formatPrice( model.output_price_per_m );
|
|
|
|
|
priceTitle = 'Input / Output per 1M tokens';
|
|
|
|
|
metaRight = '<span class="ab-mp-ctx" title="Context window">' + formatContext(model.context_length) + '</span>';
|
|
|
|
|
metaRight = '<span class="ab-mp-ctx" title="Context window">' + formatContext( model.context_length ) + '</span>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
'<div class="ab-mp-row' + deprecated + '" data-model-id="' + escAttr(model.id) + '">' +
|
|
|
|
|
'<div class="ab-mp-row' + deprecated + '" data-model-id="' + escAttr( model.id ) + '">' +
|
|
|
|
|
'<div class="ab-mp-row-main">' +
|
|
|
|
|
'<span class="ab-mp-name">' + escHtml(model.name) + '</span>' +
|
|
|
|
|
'<span class="ab-mp-provider">' + escHtml(model.provider) + '</span>' +
|
|
|
|
|
(model.deprecated ? '<span class="ab-mp-dep-badge">deprecated</span>' : '') +
|
|
|
|
|
'<span class="ab-mp-name">' + escHtml( model.name ) + '</span>' +
|
|
|
|
|
'<span class="ab-mp-provider">' + escHtml( model.provider ) + '</span>' +
|
|
|
|
|
( model.deprecated ? '<span class="ab-mp-dep-badge">deprecated</span>' : '' ) +
|
|
|
|
|
'</div>' +
|
|
|
|
|
'<div class="ab-mp-row-meta">' +
|
|
|
|
|
'<span class="ab-mp-price" title="' + priceTitle + '">' + priceHtml + '</span>' +
|
|
|
|
|
@@ -140,14 +183,14 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Minimal HTML escaping for display. */
|
|
|
|
|
function escHtml(str) {
|
|
|
|
|
if (!str) return '';
|
|
|
|
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
|
|
|
function escHtml( str ) {
|
|
|
|
|
if ( ! str ) return '';
|
|
|
|
|
return str.replace( /&/g, '&' ).replace( /</g, '<' ).replace( />/g, '>' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Escape a string for use in an HTML attribute. */
|
|
|
|
|
function escAttr(str) {
|
|
|
|
|
return escHtml(str).replace(/"/g, '"');
|
|
|
|
|
function escAttr( str ) {
|
|
|
|
|
return escHtml( str ).replace( /"/g, '"' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -155,16 +198,16 @@
|
|
|
|
|
*
|
|
|
|
|
* @param {jQuery} $trigger The .ab-mp-trigger button that was clicked.
|
|
|
|
|
*/
|
|
|
|
|
function openPicker($trigger) {
|
|
|
|
|
var fieldId = $trigger.data('field-id');
|
|
|
|
|
var capability = $trigger.data('capability') || '';
|
|
|
|
|
function openPicker( $trigger ) {
|
|
|
|
|
var fieldId = $trigger.data( 'field-id' );
|
|
|
|
|
var capability = $trigger.data( 'capability' ) || '';
|
|
|
|
|
|
|
|
|
|
// Build overlay + popup structure.
|
|
|
|
|
var isImageCap = capability === 'image_generation';
|
|
|
|
|
var priceLabel = isImageCap ? 'Cost per generated image' : 'Prices per 1M tokens (in / out)';
|
|
|
|
|
|
|
|
|
|
var $overlay = $('<div class="ab-mp-overlay"></div>');
|
|
|
|
|
var $popup = $(
|
|
|
|
|
// ── Build overlay + popup ───────────────────────────────────────
|
|
|
|
|
var $overlay = $( '<div class="ab-mp-overlay"></div>' );
|
|
|
|
|
var $popup = $(
|
|
|
|
|
'<div class="ab-mp-popup">' +
|
|
|
|
|
'<div class="ab-mp-header">' +
|
|
|
|
|
'<span class="ab-mp-title">Select Model</span>' +
|
|
|
|
|
@@ -173,6 +216,13 @@
|
|
|
|
|
'<div class="ab-mp-search-wrap">' +
|
|
|
|
|
'<input type="text" class="ab-mp-search" placeholder="Search models…" autocomplete="off" />' +
|
|
|
|
|
'</div>' +
|
|
|
|
|
'<div class="ab-mp-col-headers">' +
|
|
|
|
|
'<button type="button" class="ab-mp-col-header ab-mp-col-model" data-col="name">Model <span class="ab-mp-sort-arrow"></span></button>' +
|
|
|
|
|
'<div class="ab-mp-col-headers-meta">' +
|
|
|
|
|
'<button type="button" class="ab-mp-col-header ab-mp-col-price" data-col="price">Price (in / out) <span class="ab-mp-sort-arrow"></span></button>' +
|
|
|
|
|
'<button type="button" class="ab-mp-col-header ab-mp-col-ctx" data-col="context">Context <span class="ab-mp-sort-arrow"></span></button>' +
|
|
|
|
|
'</div>' +
|
|
|
|
|
'</div>' +
|
|
|
|
|
'<div class="ab-mp-list"><div class="ab-mp-loading">Loading models…</div></div>' +
|
|
|
|
|
'<div class="ab-mp-footer">' +
|
|
|
|
|
'<span class="ab-mp-count"></span>' +
|
|
|
|
|
@@ -181,105 +231,132 @@
|
|
|
|
|
'</div>'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$overlay.append($popup);
|
|
|
|
|
$('body').append($overlay);
|
|
|
|
|
$overlay.append( $popup );
|
|
|
|
|
$( 'body' ).append( $overlay );
|
|
|
|
|
|
|
|
|
|
var $search = $popup.find('.ab-mp-search');
|
|
|
|
|
var $list = $popup.find('.ab-mp-list');
|
|
|
|
|
var $count = $popup.find('.ab-mp-count');
|
|
|
|
|
var allModels = [];
|
|
|
|
|
var $search = $popup.find( '.ab-mp-search' );
|
|
|
|
|
var $list = $popup.find( '.ab-mp-list' );
|
|
|
|
|
var $count = $popup.find( '.ab-mp-count' );
|
|
|
|
|
var $colHeaders = $popup.find( '.ab-mp-col-headers' );
|
|
|
|
|
var allModels = [];
|
|
|
|
|
var currentQuery = '';
|
|
|
|
|
|
|
|
|
|
// Focus search after render.
|
|
|
|
|
setTimeout(function () { $search.focus(); }, 50);
|
|
|
|
|
setTimeout( function () { $search.focus(); }, 50 );
|
|
|
|
|
|
|
|
|
|
// Fetch and render model list.
|
|
|
|
|
fetchModels(capability).then(function (models) {
|
|
|
|
|
allModels = models;
|
|
|
|
|
renderList(models, '');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/** Render filtered model list. */
|
|
|
|
|
function renderList(models, query) {
|
|
|
|
|
var filtered = models;
|
|
|
|
|
if (query) {
|
|
|
|
|
var q = query.toLowerCase();
|
|
|
|
|
filtered = models.filter(function (m) {
|
|
|
|
|
return (m.name && m.name.toLowerCase().indexOf(q) !== -1) ||
|
|
|
|
|
(m.id && m.id.toLowerCase().indexOf(q) !== -1) ||
|
|
|
|
|
(m.provider && m.provider.toLowerCase().indexOf(q) !== -1);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort: non-deprecated first, then by provider + name.
|
|
|
|
|
filtered.sort(function (a, b) {
|
|
|
|
|
if (a.deprecated !== b.deprecated) return a.deprecated ? 1 : -1;
|
|
|
|
|
var pa = (a.provider || '').toLowerCase();
|
|
|
|
|
var pb = (b.provider || '').toLowerCase();
|
|
|
|
|
if (pa !== pb) return pa < pb ? -1 : 1;
|
|
|
|
|
return (a.name || '').toLowerCase().localeCompare((b.name || '').toLowerCase());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (filtered.length === 0) {
|
|
|
|
|
$list.html('<div class="ab-mp-empty">No models found.</div>');
|
|
|
|
|
} else {
|
|
|
|
|
var html = '';
|
|
|
|
|
for (var i = 0; i < filtered.length; i++) {
|
|
|
|
|
html += renderModelRow(filtered[i]);
|
|
|
|
|
// ── Sort header indicators ──────────────────────────────────────
|
|
|
|
|
/** Update active-column highlight and sort arrow glyphs. */
|
|
|
|
|
function updateSortHeaders() {
|
|
|
|
|
$colHeaders.find( '.ab-mp-col-header' ).each( function () {
|
|
|
|
|
var col = $( this ).data( 'col' );
|
|
|
|
|
var $arrow = $( this ).find( '.ab-mp-sort-arrow' );
|
|
|
|
|
if ( col === sortState.col ) {
|
|
|
|
|
$( this ).addClass( 'ab-mp-col-active' );
|
|
|
|
|
$arrow.text( sortState.dir === 'asc' ? ' ▲' : ' ▼' );
|
|
|
|
|
} else {
|
|
|
|
|
$( this ).removeClass( 'ab-mp-col-active' );
|
|
|
|
|
$arrow.text( ' ⇅' );
|
|
|
|
|
}
|
|
|
|
|
$list.html(html);
|
|
|
|
|
}
|
|
|
|
|
$count.text(filtered.length + ' model' + (filtered.length !== 1 ? 's' : ''));
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Search input handler (debounced).
|
|
|
|
|
// ── Render ──────────────────────────────────────────────────────
|
|
|
|
|
/** Render filtered + sorted model list. */
|
|
|
|
|
function renderList( models, query ) {
|
|
|
|
|
var filtered = models;
|
|
|
|
|
if ( query ) {
|
|
|
|
|
var q = query.toLowerCase();
|
|
|
|
|
filtered = models.filter( function ( m ) {
|
|
|
|
|
return ( m.name && m.name.toLowerCase().indexOf( q ) !== -1 ) ||
|
|
|
|
|
( m.id && m.id.toLowerCase().indexOf( q ) !== -1 ) ||
|
|
|
|
|
( m.provider && m.provider.toLowerCase().indexOf( q ) !== -1 );
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filtered = applySort( filtered );
|
|
|
|
|
|
|
|
|
|
if ( filtered.length === 0 ) {
|
|
|
|
|
$list.html( '<div class="ab-mp-empty">No models found.</div>' );
|
|
|
|
|
} else {
|
|
|
|
|
var html = '';
|
|
|
|
|
for ( var i = 0; i < filtered.length; i++ ) {
|
|
|
|
|
html += renderModelRow( filtered[ i ] );
|
|
|
|
|
}
|
|
|
|
|
$list.html( html );
|
|
|
|
|
}
|
|
|
|
|
$count.text( filtered.length + ' model' + ( filtered.length !== 1 ? 's' : '' ) );
|
|
|
|
|
updateSortHeaders();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initial render after data loads.
|
|
|
|
|
fetchModels( capability ).then( function ( models ) {
|
|
|
|
|
allModels = models;
|
|
|
|
|
renderList( allModels, currentQuery );
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
// ── Column header sort clicks ───────────────────────────────────
|
|
|
|
|
$colHeaders.on( 'click', '.ab-mp-col-header', function () {
|
|
|
|
|
var col = $( this ).data( 'col' );
|
|
|
|
|
if ( sortState.col === col ) {
|
|
|
|
|
sortState.dir = sortState.dir === 'asc' ? 'desc' : 'asc';
|
|
|
|
|
} else {
|
|
|
|
|
sortState.col = col;
|
|
|
|
|
sortState.dir = 'asc';
|
|
|
|
|
}
|
|
|
|
|
renderList( allModels, currentQuery );
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
// ── Search (debounced) ──────────────────────────────────────────
|
|
|
|
|
var searchTimer = null;
|
|
|
|
|
$search.on('input', function () {
|
|
|
|
|
var q = $(this).val();
|
|
|
|
|
clearTimeout(searchTimer);
|
|
|
|
|
searchTimer = setTimeout(function () {
|
|
|
|
|
renderList(allModels, q);
|
|
|
|
|
}, 150);
|
|
|
|
|
});
|
|
|
|
|
$search.on( 'input', function () {
|
|
|
|
|
currentQuery = $( this ).val();
|
|
|
|
|
clearTimeout( searchTimer );
|
|
|
|
|
searchTimer = setTimeout( function () {
|
|
|
|
|
renderList( allModels, currentQuery );
|
|
|
|
|
}, 150 );
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
// Select a model.
|
|
|
|
|
$list.on('click', '.ab-mp-row', function () {
|
|
|
|
|
var id = $(this).data('model-id');
|
|
|
|
|
var model = allModels.find(function (m) { return m.id === id; });
|
|
|
|
|
if (!model) return;
|
|
|
|
|
// ── Select a model ──────────────────────────────────────────────
|
|
|
|
|
$list.on( 'click', '.ab-mp-row', function () {
|
|
|
|
|
var id = $( this ).data( 'model-id' );
|
|
|
|
|
var model = null;
|
|
|
|
|
for ( var i = 0; i < allModels.length; i++ ) {
|
|
|
|
|
if ( allModels[ i ].id === id ) { model = allModels[ i ]; break; }
|
|
|
|
|
}
|
|
|
|
|
if ( ! model ) return;
|
|
|
|
|
|
|
|
|
|
// Update hidden input + display.
|
|
|
|
|
$('#' + fieldId).val(model.id);
|
|
|
|
|
$trigger.find('.ab-mp-display-name').text(model.name || model.id);
|
|
|
|
|
$( '#' + fieldId ).val( model.id );
|
|
|
|
|
$trigger.find( '.ab-mp-display-name' ).text( model.name || model.id );
|
|
|
|
|
|
|
|
|
|
var isImageModel = model.cost_per_image !== undefined && model.cost_per_image > 0;
|
|
|
|
|
var priceText = isImageModel
|
|
|
|
|
? '$' + model.cost_per_image.toFixed(4) + '/image'
|
|
|
|
|
: formatPrice(model.input_price_per_m) + ' / ' + formatPrice(model.output_price_per_m);
|
|
|
|
|
$trigger.find('.ab-mp-display-price').text(priceText);
|
|
|
|
|
var priceText = isImageModel
|
|
|
|
|
? '$' + model.cost_per_image.toFixed( 4 ) + '/image'
|
|
|
|
|
: formatPrice( model.input_price_per_m ) + ' / ' + formatPrice( model.output_price_per_m );
|
|
|
|
|
$trigger.find( '.ab-mp-display-price' ).text( priceText );
|
|
|
|
|
closePicker();
|
|
|
|
|
});
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
// Close handlers.
|
|
|
|
|
// ── Close ───────────────────────────────────────────────────────
|
|
|
|
|
function closePicker() {
|
|
|
|
|
$overlay.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$popup.find('.ab-mp-close').on('click', closePicker);
|
|
|
|
|
$overlay.on('click', function (e) {
|
|
|
|
|
if ($(e.target).is('.ab-mp-overlay')) closePicker();
|
|
|
|
|
});
|
|
|
|
|
$(document).on('keydown.abModelPicker', function (e) {
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
$popup.find( '.ab-mp-close' ).on( 'click', closePicker );
|
|
|
|
|
$overlay.on( 'click', function ( e ) {
|
|
|
|
|
if ( $( e.target ).is( '.ab-mp-overlay' ) ) closePicker();
|
|
|
|
|
} );
|
|
|
|
|
$( document ).on( 'keydown.abModelPicker', function ( e ) {
|
|
|
|
|
if ( e.key === 'Escape' ) {
|
|
|
|
|
closePicker();
|
|
|
|
|
$(document).off('keydown.abModelPicker');
|
|
|
|
|
$( document ).off( 'keydown.abModelPicker' );
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bind click on all model picker triggers.
|
|
|
|
|
$(document).on('click', '.ab-mp-trigger', function (e) {
|
|
|
|
|
$( document ).on( 'click', '.ab-mp-trigger', function ( e ) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
openPicker($(this));
|
|
|
|
|
});
|
|
|
|
|
openPicker( $( this ) );
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
})(jQuery);
|
|
|
|
|
} )( jQuery );
|
|
|
|
|
|