PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.14
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.14
2.0.14 2.0.13 2.0.12 2.0.11 2.0.10 2.0.9 trunk 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8
blockenberg / assets / js / ai-settings.js

ai-settings.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.14, at assets/js/ai-settings.js

380 lines 18.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 ( function () {
2 'use strict';
3 window.bkbgAISettings = window.bkbgAISettings || {};
4 var cfg = window.bkbgAISettings;
5 var __ = wp.i18n.__;
6 var sprintf = wp.i18n.sprintf;
7
8 function groupedCatalogModels( models, query ) {
9 var tokens = String( query || '' ).toLocaleLowerCase().trim().split( /\s+/ ).filter( Boolean );
10 var groups = { free: [], paid: [] };
11 ( models || [] ).filter( function ( item ) {
12 if ( ! item || typeof item.id !== 'string' ) { return false; }
13 var haystack = ( String( item.name || '' ) + ' ' + item.id ).toLocaleLowerCase();
14 return tokens.every( function ( token ) { return haystack.indexOf( token ) !== -1; } );
15 } ).forEach( function ( item ) {
16 groups[ item.free === true ? 'free' : 'paid' ].push( item );
17 } );
18 return groups;
19 }
20 cfg.groupedChatModels = groupedCatalogModels;
21 cfg.groupedCatalogModels = groupedCatalogModels;
22
23 var form = document.getElementById( 'bkbg-ai-settings-form' );
24 if ( ! form ) { return; }
25
26 var key = document.getElementById( 'bkbg-ai-key' );
27 var removeKey = document.getElementById( 'bkbg-ai-remove-key' );
28 var test = document.getElementById( 'bkbg-ai-test' );
29 var testResult = document.getElementById( 'bkbg-ai-test-result' );
30 var testing = false;
31 function syncKey() {
32 var removing = !! ( removeKey && removeKey.checked );
33 if ( key ) { key.disabled = removing; }
34 test.disabled = testing || removing || ( ! cfg.configured && ( ! key || ! key.value.trim() ) );
35 }
36 if ( key ) { key.addEventListener( 'input', syncKey ); }
37 if ( removeKey ) { removeKey.addEventListener( 'change', syncKey ); }
38 syncKey();
39
40 test.addEventListener( 'click', function () {
41 testing = true;
42 syncKey();
43 testResult.className = 'bkbg-ai-inline-status';
44 testResult.textContent = __( 'Checking your OpenRouter connection…', 'blockenberg' );
45 var body = new FormData();
46 body.append( 'action', 'bkbg_ai_test_key' );
47 body.append( 'nonce', cfg.testNonce );
48 if ( key && key.value.trim() ) { body.append( 'api_key', key.value.trim() ); }
49 window.fetch( window.ajaxurl, { method: 'POST', credentials: 'same-origin', body: body } )
50 .then( function ( response ) { return response.json(); } )
51 .then( function ( json ) {
52 var ok = !! ( json && json.success );
53 testResult.textContent = json && json.data && json.data.message || __( 'The connection could not be verified. Please try again.', 'blockenberg' );
54 testResult.classList.add( ok ? 'is-success' : 'is-error' );
55 if ( ok && ! ( json.data && json.data.unsaved ) ) {
56 var status = document.getElementById( 'bkbg-ai-connection-status' );
57 status.classList.add( 'is-configured' );
58 status.lastElementChild.textContent = __( 'Connected', 'blockenberg' );
59 }
60 } )
61 .catch( function () {
62 testResult.classList.add( 'is-error' );
63 testResult.textContent = __( 'The connection could not be verified. Check your network and try again.', 'blockenberg' );
64 } )
65 .then( function () { testing = false; syncKey(); } );
66 } );
67
68 var customOption = '__custom__';
69
70 function bindModelPicker( options ) {
71 var input = options.input;
72 var customWrap = options.customWrap;
73 var loading = options.loading;
74 var pricing = options.pricing;
75 var combobox = options.combobox;
76 var trigger = options.trigger;
77 var triggerLabel = options.triggerLabel;
78 var panel = options.panel;
79 var search = options.search;
80 var results = options.results;
81 var pickerWrap = options.pickerWrap;
82 var models = [];
83 var customValue = input.value;
84 var selectedId = customOption;
85 var menuOpen = false;
86 var activeIndex = 0;
87 var disabled = false;
88
89 function selectedModel() {
90 return models.find( function ( item ) { return item.id === selectedId; } );
91 }
92
93 function markDirty() {
94 input.dispatchEvent( new Event( 'input', { bubbles: true } ) );
95 }
96
97 function showModel() {
98 input.setCustomValidity( '' );
99 var custom = selectedId === customOption;
100 customWrap.hidden = ! custom;
101 if ( ! custom ) { input.value = selectedId; }
102 var model = selectedModel();
103 pricing.textContent = options.formatPrice( model, custom );
104 triggerLabel.textContent = custom ? __( 'Custom model ID…', 'blockenberg' ) : ( model && model.name ? model.name : selectedId );
105 trigger.title = custom ? '' : selectedId;
106 }
107
108 function optionNodes() {
109 return Array.prototype.slice.call( results.querySelectorAll( '[data-model-id]' ) );
110 }
111
112 function setActive( index ) {
113 var opts = optionNodes();
114 if ( ! opts.length ) {
115 activeIndex = 0;
116 search.removeAttribute( 'aria-activedescendant' );
117 return;
118 }
119 activeIndex = ( index + opts.length ) % opts.length;
120 opts.forEach( function ( option, i ) {
121 option.id = options.optionIdPrefix + i;
122 option.classList.toggle( 'is-active', i === activeIndex );
123 } );
124 search.setAttribute( 'aria-activedescendant', opts[ activeIndex ].id );
125 if ( opts[ activeIndex ].scrollIntoView ) {
126 opts[ activeIndex ].scrollIntoView( { block: 'nearest' } );
127 }
128 }
129
130 function renderResults() {
131 var groups = groupedCatalogModels( models, search.value );
132 var fragment = document.createDocumentFragment();
133 [ 'free', 'paid' ].forEach( function ( group ) {
134 if ( ! groups[ group ].length ) { return; }
135 var heading = document.createElement( 'div' );
136 heading.className = 'bkbg-ai-model-combobox__group';
137 var title = document.createElement( 'span' );
138 title.textContent = group === 'free' ? __( 'Free models', 'blockenberg' ) : __( 'Paid models', 'blockenberg' );
139 var count = document.createElement( 'span' );
140 count.textContent = String( groups[ group ].length );
141 heading.appendChild( title );
142 heading.appendChild( count );
143 fragment.appendChild( heading );
144 groups[ group ].forEach( function ( item ) {
145 var option = document.createElement( 'button' );
146 option.type = 'button';
147 option.className = 'bkbg-ai-model-combobox__option' + ( item.id === selectedId ? ' is-selected' : '' );
148 option.setAttribute( 'role', 'option' );
149 option.setAttribute( 'data-model-id', item.id );
150 option.setAttribute( 'aria-selected', item.id === selectedId ? 'true' : 'false' );
151 var name = document.createElement( 'span' );
152 name.textContent = item.name || item.id;
153 var id = document.createElement( 'small' );
154 id.textContent = item.id;
155 option.appendChild( name );
156 option.appendChild( id );
157 fragment.appendChild( option );
158 } );
159 } );
160 if ( ! groups.free.length && ! groups.paid.length ) {
161 var empty = document.createElement( 'p' );
162 empty.className = 'bkbg-ai-model-combobox__empty';
163 empty.textContent = __( 'No matching models.', 'blockenberg' );
164 fragment.appendChild( empty );
165 }
166 var custom = document.createElement( 'button' );
167 custom.type = 'button';
168 custom.className = 'bkbg-ai-model-combobox__option' + ( selectedId === customOption ? ' is-selected' : '' );
169 custom.setAttribute( 'role', 'option' );
170 custom.setAttribute( 'data-model-id', customOption );
171 custom.setAttribute( 'aria-selected', selectedId === customOption ? 'true' : 'false' );
172 custom.textContent = __( 'Custom model ID…', 'blockenberg' );
173 fragment.appendChild( custom );
174 results.replaceChildren( fragment );
175 var opts = optionNodes();
176 var selected = opts.findIndex( function ( option ) { return option.getAttribute( 'data-model-id' ) === selectedId; } );
177 setActive( selected === -1 ? 0 : selected );
178 }
179
180 function closeMenu( restoreFocus ) {
181 if ( ! menuOpen ) { return; }
182 menuOpen = false;
183 panel.hidden = true;
184 combobox.classList.remove( 'is-open' );
185 trigger.setAttribute( 'aria-expanded', 'false' );
186 search.value = '';
187 search.removeAttribute( 'aria-activedescendant' );
188 if ( restoreFocus !== false ) { trigger.focus(); }
189 }
190
191 function openMenu() {
192 if ( menuOpen || disabled ) { return; }
193 menuOpen = true;
194 panel.hidden = false;
195 combobox.classList.add( 'is-open' );
196 trigger.setAttribute( 'aria-expanded', 'true' );
197 search.value = '';
198 renderResults();
199 search.focus();
200 }
201
202 function choose( id ) {
203 if ( id === customOption ) {
204 selectedId = customOption;
205 input.value = customValue;
206 } else {
207 selectedId = id;
208 customValue = input.value;
209 input.value = id;
210 }
211 showModel();
212 markDirty();
213 closeMenu();
214 }
215
216 function setDisabled( next ) {
217 disabled = !! next;
218 trigger.disabled = disabled;
219 input.disabled = disabled;
220 if ( disabled ) { closeMenu( false ); }
221 }
222
223 input.addEventListener( 'input', function () { customValue = input.value; } );
224 trigger.addEventListener( 'click', function () {
225 if ( menuOpen ) { closeMenu(); } else { openMenu(); }
226 } );
227 search.addEventListener( 'input', function () { renderResults(); } );
228 search.addEventListener( 'keydown', function ( event ) {
229 var opts = optionNodes();
230 if ( event.key === 'ArrowDown' ) {
231 event.preventDefault();
232 setActive( activeIndex + 1 );
233 } else if ( event.key === 'ArrowUp' ) {
234 event.preventDefault();
235 setActive( activeIndex - 1 );
236 } else if ( event.key === 'Enter' ) {
237 event.preventDefault();
238 if ( opts[ activeIndex ] ) { choose( opts[ activeIndex ].getAttribute( 'data-model-id' ) ); }
239 } else if ( event.key === 'Escape' ) {
240 event.preventDefault();
241 closeMenu();
242 } else if ( event.key === 'Tab' ) {
243 closeMenu( false );
244 }
245 } );
246 results.addEventListener( 'click', function ( event ) {
247 var option = event.target.closest( '[data-model-id]' );
248 if ( option ) { choose( option.getAttribute( 'data-model-id' ) ); }
249 } );
250 document.addEventListener( 'pointerdown', function ( event ) {
251 if ( menuOpen && ! combobox.contains( event.target ) ) { closeMenu( false ); }
252 } );
253
254 loading.textContent = options.loadingText;
255 window.fetch( cfg.restBase + options.endpoint, {
256 credentials: 'same-origin', headers: { 'X-WP-Nonce': cfg.nonce }
257 } ).then( function ( response ) {
258 if ( ! response.ok ) { throw new Error( 'models unavailable' ); }
259 return response.json();
260 } ).then( function ( list ) {
261 if ( ! Array.isArray( list ) || ! list.length ) { throw new Error( 'models unavailable' ); }
262 models = list.filter( function ( item ) { return item && typeof item.id === 'string' && typeof item.name === 'string'; } );
263 models.sort( function ( left, right ) {
264 if ( !! left.free !== !! right.free ) { return left.free ? -1 : 1; }
265 return left.name.localeCompare( right.name, undefined, { sensitivity: 'base' } ) || left.id.localeCompare( right.id );
266 } );
267 selectedId = models.some( function ( item ) { return item.id === input.value; } ) ? input.value : customOption;
268 pickerWrap.hidden = false;
269 loading.hidden = true;
270 showModel();
271 renderResults();
272 } ).catch( function () {
273 loading.textContent = options.unavailableText;
274 customWrap.hidden = false;
275 } );
276
277 return { setDisabled: setDisabled };
278 }
279
280 function formatChatPrice( model, custom ) {
281 if ( model && model.free ) {
282 return __( 'Free model · Not guaranteed — they often error. Fine for small fixes.', 'blockenberg' );
283 }
284 if ( model && model.pricing && typeof model.pricing.prompt === 'number' && typeof model.pricing.completion === 'number' && model.pricing.prompt >= 0 && model.pricing.completion >= 0 ) {
285 var inputPrice = ( model.pricing.prompt * 1000000 ).toLocaleString( undefined, { maximumFractionDigits: 4 } );
286 var outputPrice = ( model.pricing.completion * 1000000 ).toLocaleString( undefined, { maximumFractionDigits: 4 } );
287 return sprintf( __( '$%1$s input / $%2$s output per 1M tokens · Other charges may apply', 'blockenberg' ), inputPrice, outputPrice );
288 }
289 return custom
290 ? __( 'Enter an OpenRouter model ID that supports tool calling.', 'blockenberg' )
291 : __( 'Check current pricing on OpenRouter before use.', 'blockenberg' );
292 }
293
294 function formatImagePrice( model, custom ) {
295 if ( model && model.free ) {
296 return __( 'Free model · Not guaranteed — they often error. Fine for small fixes.', 'blockenberg' );
297 }
298 if ( model && model.pricing && typeof model.pricing.image_output === 'number' && model.pricing.image_output >= 0 ) {
299 var imagePrice = ( model.pricing.image_output * 1000000 ).toLocaleString( undefined, { maximumFractionDigits: 4 } );
300 return sprintf( __( '$%s image output per 1M tokens · Other charges may apply', 'blockenberg' ), imagePrice );
301 }
302 return custom
303 ? __( 'Enter an OpenRouter model ID that can generate images.', 'blockenberg' )
304 : __( 'Check current pricing on OpenRouter before use.', 'blockenberg' );
305 }
306
307 var modelInput = document.getElementById( 'bkbg-ai-model' );
308 var imageModel = document.getElementById( 'bkbg-ai-image-model' );
309 var imageWrap = document.getElementById( 'bkbg-ai-image-model-wrap' );
310
311 bindModelPicker( {
312 input: modelInput,
313 customWrap: document.getElementById( 'bkbg-ai-custom-model-wrap' ),
314 loading: document.getElementById( 'bkbg-ai-model-loading' ),
315 pricing: document.getElementById( 'bkbg-ai-model-price' ),
316 combobox: document.getElementById( 'bkbg-ai-model-combobox' ),
317 trigger: document.getElementById( 'bkbg-ai-model-trigger' ),
318 triggerLabel: document.getElementById( 'bkbg-ai-model-trigger-label' ),
319 panel: document.getElementById( 'bkbg-ai-model-panel' ),
320 search: document.getElementById( 'bkbg-ai-model-search' ),
321 results: document.getElementById( 'bkbg-ai-model-results' ),
322 pickerWrap: document.getElementById( 'bkbg-ai-model-picker-wrap' ),
323 optionIdPrefix: 'bkbg-ai-model-opt-',
324 endpoint: '/ai/models',
325 formatPrice: formatChatPrice,
326 loadingText: __( 'Loading available models…', 'blockenberg' ),
327 unavailableText: __( 'The model list is unavailable. You can still enter a model ID and save your settings.', 'blockenberg' )
328 } );
329
330 var imagePicker = bindModelPicker( {
331 input: imageModel,
332 customWrap: document.getElementById( 'bkbg-ai-image-custom-model-wrap' ),
333 loading: document.getElementById( 'bkbg-ai-image-model-loading' ),
334 pricing: document.getElementById( 'bkbg-ai-image-model-price' ),
335 combobox: document.getElementById( 'bkbg-ai-image-model-combobox' ),
336 trigger: document.getElementById( 'bkbg-ai-image-model-trigger' ),
337 triggerLabel: document.getElementById( 'bkbg-ai-image-model-trigger-label' ),
338 panel: document.getElementById( 'bkbg-ai-image-model-panel' ),
339 search: document.getElementById( 'bkbg-ai-image-model-search' ),
340 results: document.getElementById( 'bkbg-ai-image-model-results' ),
341 pickerWrap: document.getElementById( 'bkbg-ai-image-model-picker-wrap' ),
342 optionIdPrefix: 'bkbg-ai-image-opt-',
343 endpoint: '/ai/image-models',
344 formatPrice: formatImagePrice,
345 loadingText: __( 'Loading image models…', 'blockenberg' ),
346 unavailableText: __( 'The image model list is unavailable. You can still enter a model ID and save your settings.', 'blockenberg' )
347 } );
348
349 function syncImages() {
350 var source = form.querySelector( 'input[name="image_source"]:checked' );
351 var generate = source && source.value === 'openrouter';
352 imagePicker.setDisabled( ! generate );
353 imageWrap.classList.toggle( 'is-inactive', ! generate );
354 }
355 form.querySelectorAll( 'input[name="image_source"]' ).forEach( function ( input ) {
356 input.addEventListener( 'change', syncImages );
357 } );
358 syncImages();
359
360 form.addEventListener( 'input', function () {
361 document.getElementById( 'bkbg-ai-save-status' ).textContent = __( 'You have unsaved changes.', 'blockenberg' );
362 } );
363 form.addEventListener( 'submit', function ( event ) {
364 var invalid = [ modelInput, imageModel ].find( function ( field ) {
365 return ! field.disabled && ! /^[a-z0-9][a-z0-9._:/+@-]{0,199}$/i.test( field.value.trim() );
366 } );
367 if ( invalid ) {
368 event.preventDefault();
369 if ( invalid === modelInput ) { document.getElementById( 'bkbg-ai-custom-model-wrap' ).hidden = false; }
370 if ( invalid === imageModel ) { document.getElementById( 'bkbg-ai-image-custom-model-wrap' ).hidden = false; }
371 invalid.setCustomValidity( __( 'Enter a valid OpenRouter model ID.', 'blockenberg' ) );
372 invalid.reportValidity();
373 invalid.addEventListener( 'input', function clearValidity() {
374 invalid.setCustomValidity( '' );
375 invalid.removeEventListener( 'input', clearValidity );
376 } );
377 }
378 } );
379 } )();
380