Changeset 12633
- Timestamp:
- 06/08/2023 03:07:08 AM (3 years ago)
- Location:
- sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers
- Files:
-
- 2 edited
-
includes/class-gp-translation-helpers.php (modified) (1 diff)
-
js/editor.js (modified) (17 diffs)
Legend:
- Unmodified
- Added
- Removed
-
sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/includes/class-gp-translation-helpers.php
r12629 r12633 402 402 '$gp_comment_feedback_settings', 403 403 array( 404 'url' => admin_url( 'admin-ajax.php' ), 405 'nonce' => wp_create_nonce( 'gp_comment_feedback' ), 406 'locale_slug' => $translation_set['locale_slug'], 407 'language' => $gp_locale ? $gp_locale->english_name : 'Unknown', 408 'has_openai_key' => !! apply_filters( 'gp_get_openai_key', null ), 409 'comment_reasons' => Helper_Translation_Discussion::get_comment_reasons( $translation_set['locale_slug'] ), 404 'url' => admin_url( 'admin-ajax.php' ), 405 'nonce' => wp_create_nonce( 'gp_comment_feedback' ), 406 'locale_slug' => $translation_set['locale_slug'], 407 'language' => $gp_locale ? $gp_locale->english_name : 'Unknown', 408 'openai_key' => apply_filters( 'gp_get_openai_key', null ), 409 'openai_temperature' => apply_filters( 'gp_get_openai_temperature', 0.8 ), 410 'comment_reasons' => Helper_Translation_Discussion::get_comment_reasons( $translation_set['locale_slug'] ), 410 411 ) 411 412 ); -
sites/trunk/wordpress.org/public_html/wp-content/plugins/gp-translation-helpers/js/editor.js
r12629 r12633 1 /* global $gp, $gp_translation_helpers_editor, wpApiSettings, $gp_comment_feedback_settings, console, $gp_editor_options */1 /* global $gp, $gp_translation_helpers_editor, wpApiSettings, $gp_comment_feedback_settings, console, $gp_editor_options, EventSource */ 2 2 /* eslint camelcase: "off" */ 3 3 jQuery( function( $ ) { 4 varfocusedRowId = '';4 let focusedRowId = ''; 5 5 // When a user clicks on a sidebar tab, the visible tab and div changes. 6 6 $gp.editor.table.on( 'click', '.sidebar-tabs li', function() { 7 vartab = $( this );8 vartabId = tab.attr( 'data-tab' );9 vardivId = tabId.replace( 'tab', 'div' );10 varoriginalId = tabId.replace( /[^\d-]/g, '' ).replace( /^-+/g, '' );7 const tab = $( this ); 8 const tabId = tab.attr( 'data-tab' ); 9 const divId = tabId.replace( 'tab', 'div' ); 10 const originalId = tabId.replace( /[^\d-]/g, '' ).replace( /^-+/g, '' ); 11 11 change_visible_tab( tab ); 12 12 change_visible_div( divId, originalId ); … … 17 17 // divs with the content) for the right sidebar are updated. 18 18 $gp.editor.table.on( 'focus', 'tr.editor textarea.foreign-text', function() { 19 vartr = $( this ).closest( 'tr.editor' );20 varrowId = tr.attr( 'row' );21 vartranslation_status = tr.find( '.panel-header' ).find( 'span' ).html();19 const tr = $( this ).closest( 'tr.editor' ); 20 const rowId = tr.attr( 'row' ); 21 const translation_status = tr.find( '.panel-header' ).find( 'span' ).html(); 22 22 23 23 if ( focusedRowId === rowId ) { … … 26 26 focusedRowId = rowId; 27 27 loadTabsAndDivs( tr ); 28 if ( $gp_comment_feedback_settings. has_openai_key && $gp_editor_options.can_approve && ( 'waiting' === translation_status || 'fuzzy' === translation_status ) ) {28 if ( $gp_comment_feedback_settings.openai_key && $gp_editor_options.can_approve && ( 'waiting' === translation_status || 'fuzzy' === translation_status ) ) { 29 29 fetchOpenAIReviewResponse( rowId, tr, false ); 30 30 } else { … … 34 34 35 35 $gp.editor.table.on( 'click', 'a.retry-auto-review', function( event ) { 36 vartr = $( this ).closest( 'tr.editor' );37 varrowId = tr.attr( 'row' );36 const tr = $( this ).closest( 'tr.editor' ); 37 const rowId = tr.attr( 'row' ); 38 38 event.preventDefault(); 39 39 tr.find( '.openai-review .auto-review-result' ).html( '' ); … … 44 44 // Shows/hides the reply form for a comment in the discussion. 45 45 $gp.editor.table.on( 'click', 'a.comment-reply-link', function( event ) { 46 varcommentId = $( this ).attr( 'data-commentid' );46 const commentId = $( this ).attr( 'data-commentid' ); 47 47 event.preventDefault(); 48 48 $( '#comment-reply-' + commentId ).toggle().find( 'textarea' ).focus(); … … 58 58 // to avoid creating empty posts (without comments). 59 59 function createShadowPost( formdata, submitComment ) { 60 vardata = {60 const data = { 61 61 action: 'create_shadow_post', 62 62 data: formdata, … … 68 68 type: 'POST', 69 69 url: wpApiSettings.admin_ajax_url, 70 data : data,70 data, 71 71 } 72 72 ).done( … … 80 80 // Sends the new comment or the reply to an existing comment. 81 81 $gp.editor.table.on( 'submit', '.meta.discussion .comment-form', function( e ) { 82 var$commentform = $( e.target );83 varpostId = $commentform.attr( 'id' ).split( '-' )[ 1 ];84 vardivDiscussion = $commentform.closest( '.meta.discussion' );85 varrowId = divDiscussion.attr( 'data-row-id' );86 varrequestUrl = $gp_translation_helpers_editor.translation_helper_url + rowId + '?nohc';87 88 varsubmitComment = function( formdata ) {82 const $commentform = $( e.target ); 83 const postId = $commentform.attr( 'id' ).split( '-' )[ 1 ]; 84 const divDiscussion = $commentform.closest( '.meta.discussion' ); 85 const rowId = divDiscussion.attr( 'data-row-id' ); 86 const requestUrl = $gp_translation_helpers_editor.translation_helper_url + rowId + '?nohc'; 87 88 const submitComment = function( formdata ) { 89 89 $.ajax( { 90 90 url: wpApiSettings.root + 'wp/v2/comments', 91 91 method: 'POST', 92 beforeSend : function( xhr ) {92 beforeSend( xhr ) { 93 93 xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce ); 94 94 }, … … 108 108 }; 109 109 110 varformdata = {110 const formdata = { 111 111 content: $commentform.find( 'textarea[name=comment]' ).val(), 112 112 parent: $commentform.find( 'input[name=comment_parent]' ).val(), … … 148 148 // Copies the translation from another language to the current translation. 149 149 $gp.editor.table.on( 'click', 'button.sidebar-other-locales', function() { 150 vartextToCopy = $( this ).closest( 'li' ).find( 'a' ).text();151 vartextareaToPaste = $( this ).closest( '.editor' ).find( 'textarea.foreign-text' );152 varselectionStart = textareaToPaste.get( 0 ).selectionStart;153 varselectionEnd = textareaToPaste.get( 0 ).selectionEnd;154 vartextToCopyLength = textToCopy.length;150 const textToCopy = $( this ).closest( 'li' ).find( 'a' ).text(); 151 const textareaToPaste = $( this ).closest( '.editor' ).find( 'textarea.foreign-text' ); 152 let selectionStart = textareaToPaste.get( 0 ).selectionStart; 153 let selectionEnd = textareaToPaste.get( 0 ).selectionEnd; 154 const textToCopyLength = textToCopy.length; 155 155 textareaToPaste.val( textareaToPaste.val().substring( 0, selectionStart ) + 156 156 textToCopy + … … 168 168 // table has only one, so with the double click we load the content sidebar. 169 169 // eslint-disable-next-line vars-on-top 170 varpreviewRows = $gp.editor.table.find( 'tr.preview' );170 const previewRows = $gp.editor.table.find( 'tr.preview' ); 171 171 if ( 1 === previewRows.length ) { 172 172 $( 'tr.preview td' ).trigger( 'dblclick' ); … … 179 179 */ 180 180 function change_visible_tab( tab ) { 181 vartabId = tab.attr( 'data-tab' );181 const tabId = tab.attr( 'data-tab' ); 182 182 tab.siblings().removeClass( 'current' ); 183 183 tab.parents( '.sidebar-tabs ' ).find( '.helper' ).removeClass( 'current' ); … … 207 207 */ 208 208 function add_copy_button( sidebarDiv ) { 209 varlis = $( sidebarDiv + ' .other-locales li' );209 const lis = $( sidebarDiv + ' .other-locales li' ); 210 210 lis.each( function() { 211 varhtml = $( this ).html();211 let html = $( this ).html(); 212 212 html += '<button class="sidebar-other-locales button is-small copy-suggestion"> Copy </button>'; 213 213 $( this ).html( html ); … … 221 221 */ 222 222 function loadTabsAndDivs( element ) { 223 varoriginalId = element.closest( 'tr' ).attr( 'id' ).substring( 7 );224 varrequestUrl = $gp_translation_helpers_editor.translation_helper_url + originalId + '?nohc';223 const originalId = element.closest( 'tr' ).attr( 'id' ).substring( 7 ); 224 const requestUrl = $gp_translation_helpers_editor.translation_helper_url + originalId + '?nohc'; 225 225 $.getJSON( requestUrl, function( data ) { 226 226 $( '[data-tab="sidebar-tab-discussion-' + originalId + '"]' ).html( 'Discussion (' + data[ 'helper-translation-discussion-' + originalId ].count + ')' ); … … 234 234 } 235 235 236 function EventStreamParser( onParse ) { 237 // npm eventsource-parser 238 // MIT License 239 // Copyright (c) 2023 Espen Hovlandsdal <espen@hovlandsdal.com> 240 241 // Processing state 242 let isFirstChunk; 243 let buffer; 244 let startingPosition; 245 let startingFieldLength; 246 247 // Event state 248 let eventId; 249 let eventName; 250 let data; 251 252 reset(); 253 return { feed, reset }; 254 255 function reset() { 256 isFirstChunk = true; 257 buffer = ''; 258 startingPosition = 0; 259 startingFieldLength = -1; 260 261 eventId = undefined; 262 eventName = undefined; 263 data = ''; 264 } 265 266 function feed( chunk ) { 267 buffer = buffer ? buffer + chunk : chunk; 268 269 // Strip any UTF8 byte order mark (BOM) at the start of the stream. 270 // Note that we do not strip any non - UTF8 BOM, as eventsource streams are 271 // always decoded as UTF8 as per the specification. 272 if ( isFirstChunk && hasBom( buffer ) ) { 273 buffer = buffer.slice( 3 ); 274 } 275 276 isFirstChunk = false; 277 278 // Set up chunk-specific processing state 279 const length = buffer.length; 280 let position = 0; 281 let discardTrailingNewline = false; 282 283 // Read the current buffer byte by byte 284 while ( position < length ) { 285 // EventSource allows for carriage return + line feed, which means we 286 // need to ignore a linefeed character if the previous character was a 287 // carriage return 288 // @todo refactor to reduce nesting, consider checking previous byte? 289 // @todo but consider multiple chunks etc 290 if ( discardTrailingNewline ) { 291 if ( buffer[ position ] === '\n' ) { 292 ++position; 293 } 294 discardTrailingNewline = false; 295 } 296 297 let lineLength = -1; 298 let fieldLength = startingFieldLength; 299 let character; 300 301 for ( let index = startingPosition; lineLength < 0 && index < length; ++index ) { 302 character = buffer[ index ]; 303 if ( character === ':' && fieldLength < 0 ) { 304 fieldLength = index - position; 305 } else if ( character === '\r' ) { 306 discardTrailingNewline = true; 307 lineLength = index - position; 308 } else if ( character === '\n' ) { 309 lineLength = index - position; 310 } 311 } 312 313 if ( lineLength < 0 ) { 314 startingPosition = length - position; 315 startingFieldLength = fieldLength; 316 break; 317 } else { 318 startingPosition = 0; 319 startingFieldLength = -1; 320 } 321 322 parseEventStreamLine( buffer, position, fieldLength, lineLength ); 323 324 position += lineLength + 1; 325 } 326 327 if ( position === length ) { 328 // If we consumed the entire buffer to read the event, reset the buffer 329 buffer = ''; 330 } else if ( position > 0 ) { 331 // If there are bytes left to process, set the buffer to the unprocessed 332 // portion of the buffer only 333 buffer = buffer.slice( position ); 334 } 335 } 336 337 function parseEventStreamLine( 338 lineBuffer, 339 index, 340 fieldLength, 341 lineLength 342 ) { 343 if ( lineLength === 0 ) { 344 // We reached the last line of this event 345 if ( data.length > 0 ) { 346 onParse( { 347 type: 'event', 348 id: eventId, 349 event: eventName || undefined, 350 data: data.slice( 0, -1 ), // remove trailing newline 351 } ); 352 353 data = ''; 354 eventId = undefined; 355 } 356 eventName = undefined; 357 return; 358 } 359 360 const noValue = fieldLength < 0; 361 const field = lineBuffer.slice( index, index + ( noValue ? lineLength : fieldLength ) ); 362 let step = 0; 363 364 if ( noValue ) { 365 step = lineLength; 366 } else if ( lineBuffer[ index + fieldLength + 1 ] === ' ' ) { 367 step = fieldLength + 2; 368 } else { 369 step = fieldLength + 1; 370 } 371 372 const position = index + step; 373 const valueLength = lineLength - step; 374 const value = lineBuffer.slice( position, position + valueLength ).toString(); 375 376 if ( field === 'data' ) { 377 data += value ? `${ value }\n` : '\n'; 378 } else if ( field === 'event' ) { 379 eventName = value; 380 } else if ( field === 'id' && ! value.includes( '\u0000' ) ) { 381 eventId = value; 382 } else if ( field === 'retry' ) { 383 const retry = parseInt( value, 10 ); 384 if ( ! Number.isNaN( retry ) ) { 385 onParse( { type: 'reconnect-interval', value: retry } ); 386 } 387 } 388 } 389 function hasBom( b ) { 390 return [ 239, 187, 191 ].every( ( charCode, index ) => b.charCodeAt( index ) === charCode ); 391 } 392 } 393 394 async function invokeChatGPT( prompt, response_span ) { 395 const request = { 396 model: 'gpt-3.5-turbo', 397 messages: prompt, 398 temperature: parseFloat( $gp_comment_feedback_settings.openai_temperature ), 399 stream: true, 400 }; 401 let result = ''; 402 const parser = EventStreamParser( function( event ) { 403 if ( event.type === 'event' ) { 404 if ( event.data !== '[DONE]' ) { 405 result += JSON.parse( event.data ).choices[ 0 ].delta.content || ''; 406 response_span.text( result ); 407 } 408 } else if ( event.type === 'invalid_request_error' ) { 409 response_span.text( event.value ); 410 } else if ( event.type === 'reconnect-interval' ) { 411 console.log( 'We should set reconnect interval to %d milliseconds', event.value ); 412 } 413 } ); 414 415 let response = await fetch( 416 'https://api.openai.com/v1/chat/completions', 417 { 418 headers: { 419 'Content-Type': 'application/json', 420 Authorization: `Bearer ${ $gp_comment_feedback_settings.openai_key }`, 421 }, 422 method: 'POST', 423 body: JSON.stringify( request ), 424 } 425 ); 426 427 for await (const value of response.body?.pipeThrough(new TextDecoderStream())) { 428 parser.feed(value) 429 } 430 } 431 236 432 /** 237 433 * Fetch translation review from OpenAI. … … 239 435 * @param {string} rowId The row-id attribute of the current row. 240 436 * @param {string} currentRow The current row. 241 * @param {boolean} isRetry The current row.242 437 */ 243 438 function fetchOpenAIReviewResponse( rowId, currentRow, isRetry ) { 244 var payload = {}; 245 var data = {}; 246 var original_str = currentRow.find( '.original' ); 247 var glossary_prompt = ''; 248 var translation = currentRow.find( '.foreign-text:first' ).val(); 439 const messages = []; 440 const original_str = currentRow.find( '.original' ); 441 let glossary_prompt = ''; 442 let result = ''; 249 443 250 444 $.each( $( original_str ).find( '.glossary-word' ), function( k, word ) { … … 259 453 260 454 if ( '' !== glossary_prompt ) { 261 glossary_prompt = 'You are required to follow these rules, ' + glossary_prompt + 'for words found in the English text you are translating.'; 262 } 263 payload.language = $gp_comment_feedback_settings.language; 264 payload.original = currentRow.find( '.original-raw' ).text(); 265 payload.translation = translation; 266 payload.glossary_query = glossary_prompt; 267 payload.is_retry = isRetry; 268 269 data = { 270 action: 'fetch_openai_review', 271 data: payload, 272 _ajax_nonce: $gp_comment_feedback_settings.nonce, 273 }; 274 275 $.ajax( 276 { 277 type: 'POST', 278 url: typeof window.useThinFetch !== 'undefined' && window.useThinFetch ? '/wp-content/plugins/wporg-gp-translation-suggestions/ajax-fetch-openai-review.php' : $gp_comment_feedback_settings.url, 279 data: data, 280 } 281 ).done( 282 function( response ) { 283 currentRow.find( '.openai-review .suggestions__loading-indicator' ).hide(); 284 if ( response.success ) { 285 currentRow.find( '.openai-review .auto-review-result' ).html( '<h4>Review by ChatGPT' ).append( $( '<span/>' ).text( response.data.review + ' (' + response.data.time_taken.toFixed( 2 ) + 's)' ) ); 286 } else if ( 404 === response.data.status ) { 287 currentRow.find( '.openai-review' ).hide(); 288 return; 289 } else { 290 currentRow.find( '.openai-review .auto-review-result' ).text( 'Error ' + response.data.status + ' : ' + response.data.error ); 291 } 292 currentRow.find( '.openai-review .auto-review-result' ).append( ' <a href="#" class="retry-auto-review">Retry</a>' ); 293 } 294 ).fail( 295 function( xhr, msg ) { 296 /* eslint no-console: ["error", { allow: ["error"] }] */ 297 console.error( data ); 298 } 299 ); 455 messages.push( { 456 role: 'system', 457 content: 'You are required to follow these rules, ' + glossary_prompt + 'for words found in the English text you are translating.', 458 } ); 459 } 460 messages.push( { 461 role: 'system', 462 content: ( isRetry ? 'Are you sure that ' : '' ) + 'For the english text "' + currentRow.find( '.original-raw' ).text() + '", is "' + currentRow.find( '.foreign-text:first' ).val() + '" a correct translation in ' + $gp_comment_feedback_settings.language + '? Don\'t repeat the translation if it is correct and point out differences if there are any.', 463 } ); 464 465 currentRow.find( '.openai-review .suggestions__loading-indicator' ).hide(); 466 currentRow.find( '.openai-review .auto-review-result' ).html( '<h4>Review by ChatGPT' ).append( '<span style="white-space: pre-line">' ); 467 invokeChatGPT( messages, currentRow.find( '.openai-review .auto-review-result span' ) ).then(()=>currentRow.find( '.openai-review .auto-review-result' ).append( ' <a href="#" class="retry-auto-review">Retry</a>' )); 300 468 } 301 469 } );
Note: See TracChangeset
for help on using the changeset viewer.