PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.9
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.9
4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 All 140 releases
← All changes | assets/src/js/frontend/ai-assistant.js +121 -43 4.4.0 → 4.4.9 View file →
@@ -42,8 +42,16 @@
42 42 quizCard: '.lp-ai-assistant__quiz-card',
43 43 quizOptionBtn: '.lp-ai-assistant__quiz-option',
44 44 };
45 45
46 + /**
47 + * Curriculum item types the assistant supports.
48 + *
49 + * Mirrors AIAssistantController::get_supported_item_types(). The server re-validates,
50 + * so this only avoids pointless requests.
51 + */
52 + static itemTypes = [ 'lp_lesson', 'lp_quiz' ];
53 +
46 54 init() {
47 55 if ( ! this.validateConfig() ) {
48 56 return;
49 57 }
@@ -91,8 +99,18 @@
91 99 if ( ! Number.isInteger( this.config.courseId ) || this.config.courseId <= 0 ) {
92 100 return false;
93 101 }
94 102
103 + /**
104 + * itemType is required and must come from the server-localized config. It is
105 + * never guessed from context, lessonId or the numeric ID: a course item is
106 + * identified by (courseId, itemType, itemId), and inferring one leg of that
107 + * tuple on the client would let a lesson request address a quiz record.
108 + */
109 + if ( ! AIAssistantWidget.itemTypes.includes( this.config.itemType ) ) {
110 + return false;
111 + }
112 +
95 113 this.config.itemId = itemId;
96 114 this.config.lessonId = itemId; // Backward compatibility for existing AJAX contract.
97 115 this.config.context = this.config.context === 'quiz' ? 'quiz' : 'lesson';
98 116 this.config.quizCompleted = !! this.config.quizCompleted;
@@ -420,22 +438,38 @@
420 438 localStorage.removeItem( this.storageKey );
421 439 this.setQuizInputMode( false );
422 440 }
423 441
424 - escHtml( text ) {
425 - const div = document.createElement( 'div' );
426 - div.appendChild( document.createTextNode( String( text ) ) );
427 - return div.innerHTML;
442 + /**
443 + * Create an element with a class and plain text content.
444 + *
445 + * All assistant content originates from OpenAI output, so it is built through the
446 + * DOM API only. Nothing on this path goes through innerHTML: text assigned via
447 + * textContent can never become markup, and attributes set via the DOM API can never
448 + * break out into a new attribute or event handler.
449 + *
450 + * @param {string} tag Tag name.
451 + * @param {string} className Class attribute.
452 + * @param {string} text Text content.
453 + * @return {HTMLElement} The created element.
454 + */
455 + createEl( tag, className, text = '' ) {
456 + const el = document.createElement( tag );
457 + if ( className ) {
458 + el.className = className;
459 + }
460 + if ( text !== '' ) {
461 + el.textContent = String( text );
462 + }
463 + return el;
428 464 }
429 465
430 466 appendMessage( role, text ) {
431 - const el = document.createElement( 'div' );
432 - el.className = `lp-ai-assistant__msg lp-ai-assistant__msg--${ role }`;
467 + const el = this.createEl( 'div', `lp-ai-assistant__msg lp-ai-assistant__msg--${ role }` );
468 + const label = role === 'user' ? this.config.i18n.you : this.config.i18n.assistant;
433 469
434 - const label = role === 'user' ? this.config.i18n.you : this.config.i18n.assistant;
435 - el.innerHTML =
436 - `<span class="lp-ai-assistant__msg-label">${ this.escHtml( label ) }</span>` +
437 - `<p class="lp-ai-assistant__msg-text">${ this.escHtml( text ) }</p>`;
470 + el.appendChild( this.createEl( 'span', 'lp-ai-assistant__msg-label', label ) );
471 + el.appendChild( this.createEl( 'p', 'lp-ai-assistant__msg-text', text ) );
438 472
439 473 this.elements.msgList.appendChild( el );
440 474 this.elements.msgList.scrollTop = this.elements.msgList.scrollHeight;
441 475 return el;
@@ -511,14 +545,31 @@
511 545
512 546 return true;
513 547 }
514 548
515 - renderQuizReviewOptions( review ) {
549 + /**
550 + * Build one quiz option button.
551 + *
552 + * @param {string} option Option text from model output.
553 + * @param {number} index Zero-based option index.
554 + * @param {string[]} extraClasses Additional state classes.
555 + * @return {HTMLButtonElement} The option button.
556 + */
557 + buildQuizOption( option, index, extraClasses = [] ) {
558 + const classes = [ 'lp-ai-assistant__quiz-option', ...extraClasses ].join( ' ' );
559 + const letter = String.fromCharCode( 65 + index );
560 +
561 + const btn = this.createEl( 'button', classes, `${ letter }. ${ String( option ) }` );
562 + btn.type = 'button';
563 +
564 + return btn;
565 + }
566 +
567 + buildQuizReviewOptions( review ) {
516 568 const options = Array.isArray( review.options ) ? review.options : [];
517 569
518 570 return options.map( ( option, index ) => {
519 - const letter = String.fromCharCode( 65 + index );
520 - const classes = [ 'lp-ai-assistant__quiz-option' ];
571 + const classes = [];
521 572 if ( index === review.correct_index ) {
522 573 classes.push( 'is-correct-answer' );
523 574 }
524 575
@@ -525,33 +576,47 @@
525 576 if ( index === review.selected_index ) {
526 577 classes.push( review.is_correct ? 'is-selected-correct' : 'is-selected-wrong' );
527 578 }
528 579
529 - return `<button class="${ classes.join( ' ' ) }" disabled>${ letter }. ${ this.escHtml( option ) }</button>`;
530 - } ).join( '' );
580 + const btn = this.buildQuizOption( option, index, classes );
581 + btn.disabled = true;
582 +
583 + return btn;
584 + } );
531 585 }
532 586
533 587 appendQuizReviewCard( review ) {
534 - const card = document.createElement( 'div' );
535 - card.className = 'lp-ai-assistant__quiz-card lp-ai-assistant__quiz-card--review';
536 - card.setAttribute( 'data-review-key', this.getQuizReviewKey( review ) );
588 + const card = this.createEl( 'div', 'lp-ai-assistant__quiz-card lp-ai-assistant__quiz-card--review' );
589 + card.dataset.reviewKey = this.getQuizReviewKey( review );
537 590
591 + const optionCount = Array.isArray( review.options ) ? review.options.length : 0;
592 + const total = review.total || optionCount;
593 +
594 + card.appendChild(
595 + this.createEl( 'div', 'lp-ai-assistant__quiz-head', `Question ${ review.question_index + 1 }/${ total }` )
596 + );
597 + card.appendChild( this.createEl( 'div', 'lp-ai-assistant__quiz-question', review.question || '' ) );
598 +
599 + const optionsEl = this.createEl( 'div', 'lp-ai-assistant__quiz-options' );
600 + this.buildQuizReviewOptions( review ).forEach( ( btn ) => optionsEl.appendChild( btn ) );
601 + card.appendChild( optionsEl );
602 +
538 603 const feedbackClass = review.is_correct ? 'is-correct' : 'is-wrong';
539 - const feedbackTitle = review.is_correct
540 - ? this.config.i18n.quizCorrectTitle
541 - : this.config.i18n.quizWrongTitle;
542 - const feedbackHtml =
543 - `<div class="lp-ai-assistant__quiz-feedback ${ feedbackClass }">` +
544 - `<strong>${ this.escHtml( feedbackTitle ) }</strong>` +
545 - ( review.explanation ? `<div>${ this.escHtml( review.explanation ) }</div>` : '' ) +
546 - '</div>';
604 + const feedbackEl = this.createEl( 'div', `lp-ai-assistant__quiz-feedback ${ feedbackClass }` );
605 + feedbackEl.appendChild(
606 + this.createEl(
607 + 'strong',
608 + '',
609 + review.is_correct ? this.config.i18n.quizCorrectTitle : this.config.i18n.quizWrongTitle
610 + )
611 + );
547 612
548 - card.innerHTML =
549 - `<div class="lp-ai-assistant__quiz-head">Question ${ review.question_index + 1 }/${ review.total || review.options.length }</div>` +
550 - `<div class="lp-ai-assistant__quiz-question">${ this.escHtml( review.question || '' ) }</div>` +
551 - `<div class="lp-ai-assistant__quiz-options">${ this.renderQuizReviewOptions( review ) }</div>` +
552 - feedbackHtml;
613 + if ( review.explanation ) {
614 + feedbackEl.appendChild( this.createEl( 'div', '', review.explanation ) );
615 + }
553 616
617 + card.appendChild( feedbackEl );
618 +
554 619 this.elements.msgList.appendChild( card );
555 620 }
556 621
557 622 renderQuizState() {
@@ -584,22 +649,33 @@
584 649 this.setQuizInputMode( false );
585 650 return;
586 651 }
587 652
588 - const card = document.createElement( 'div' );
589 - card.className = 'lp-ai-assistant__quiz-card lp-ai-assistant__quiz-card--active';
653 + const card = this.createEl( 'div', 'lp-ai-assistant__quiz-card lp-ai-assistant__quiz-card--active' );
654 + const options = Array.isArray( question.options ) ? question.options : [];
590 655
591 - const options = Array.isArray( question.options ) ? question.options : [];
592 - const optionsHtml = options.map( ( option, index ) => {
593 - const letter = String.fromCharCode( 65 + index );
594 - return `<button class="lp-ai-assistant__quiz-option" data-index="${ index }" data-option="${ this.escHtml( option ) }">${ letter }. ${ this.escHtml( option ) }</button>`;
595 - } ).join( '' );
656 + card.appendChild(
657 + this.createEl(
658 + 'div',
659 + 'lp-ai-assistant__quiz-head',
660 + `Question ${ currentIndex + 1 }/${ quiz.total || options.length }`
661 + )
662 + );
663 + card.appendChild( this.createEl( 'div', 'lp-ai-assistant__quiz-question', question.question || '' ) );
596 664
597 - card.innerHTML =
598 - `<div class="lp-ai-assistant__quiz-head">Question ${ currentIndex + 1 }/${ quiz.total || options.length }</div>` +
599 - `<div class="lp-ai-assistant__quiz-question">${ this.escHtml( question.question || '' ) }</div>` +
600 - `<div class="lp-ai-assistant__quiz-options">${ optionsHtml }</div>`;
665 + const optionsEl = this.createEl( 'div', 'lp-ai-assistant__quiz-options' );
666 + options.forEach( ( option, index ) => {
667 + const btn = this.buildQuizOption( option, index );
601 668
669 + // Assigned through dataset, never interpolated into an HTML attribute — model
670 + // output containing a quote cannot open a new attribute or event handler.
671 + btn.dataset.index = String( index );
672 + btn.dataset.option = String( option );
673 +
674 + optionsEl.appendChild( btn );
675 + } );
676 + card.appendChild( optionsEl );
677 +
602 678 this.elements.msgList.appendChild( card );
603 679 this.setQuizInputMode( true );
604 680 }
605 681
@@ -639,10 +715,12 @@
639 715
640 716 const dataSend = {
641 717 action: 'openai_assistant_chat',
642 718 message: text,
719 + // Composite item identity — all three legs are required server-side.
720 + course_id: this.config.courseId,
721 + item_type: this.config.itemType,
643 722 item_id: this.config.itemId,
644 - course_id: this.config.courseId,
645 723 history: contextHistory,
646 724 active_quiz_questions: this.activeQuizState || [],
647 725 action_hint: typeof actionHint === 'string' ? actionHint : '',
648 726 };