summarize.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. class SummarizeController extends window.StimulusModule.Controller {
  2. static targets = ['suggest', 'clear'];
  3. static values = {
  4. input: { default: '', type: String },
  5. type: { default: 'teaser', type: String },
  6. length: { default: 'short', type: String },
  7. };
  8. static css = /* css */ `
  9. .summarize-output {
  10. margin-top: 1rem;
  11. }
  12. .suggestion {
  13. display: block;
  14. margin-top: 0.5rem;
  15. margin-bottom: 0.5rem;
  16. border-radius: 0.25rem;
  17. padding: 0.5rem;
  18. background-color: lightblue;
  19. color: black;
  20. }
  21. `;
  22. static icon = /* html */ `
  23. <svg
  24. width="16"
  25. height="16"
  26. class="Draftail-Icon"
  27. aria-hidden="true"
  28. viewBox="0 0 576 512"
  29. fill="currentColor"
  30. >
  31. <path
  32. d="M234.7 42.7L197 56.8c-3 1.1-5 4-5 7.2s2 6.1 5 7.2l37.7 14.1L248.8 123c1.1 3 4 5 7.2 5s6.1-2 7.2-5l14.1-37.7L315 71.2c3-1.1 5-4 5-7.2s-2-6.1-5-7.2L277.3 42.7 263.2 5c-1.1-3-4-5-7.2-5s-6.1 2-7.2 5L234.7 42.7zM46.1 395.4c-18.7 18.7-18.7 49.1 0 67.9l34.6 34.6c18.7 18.7 49.1 18.7 67.9 0L529.9 116.5c18.7-18.7 18.7-49.1 0-67.9L495.3 14.1c-18.7-18.7-49.1-18.7-67.9 0L46.1 395.4zM484.6 82.6l-105 105-23.3-23.3 105-105 23.3 23.3zM7.5 117.2C3 118.9 0 123.2 0 128s3 9.1 7.5 10.8L64 160l21.2 56.5c1.7 4.5 6 7.5 10.8 7.5s9.1-3 10.8-7.5L128 160l56.5-21.2c4.5-1.7 7.5-6 7.5-10.8s-3-9.1-7.5-10.8L128 96 106.8 39.5C105.1 35 100.8 32 96 32s-9.1 3-10.8 7.5L64 96 7.5 117.2zm352 256c-4.5 1.7-7.5 6-7.5 10.8s3 9.1 7.5 10.8L416 416l21.2 56.5c1.7 4.5 6 7.5 10.8 7.5s9.1-3 10.8-7.5L480 416l56.5-21.2c4.5-1.7 7.5-6 7.5-10.8s-3-9.1-7.5-10.8L480 352l-21.2-56.5c-1.7-4.5-6-7.5-10.8-7.5s-9.1 3-10.8 7.5L416 352l-56.5 21.2z"
  33. ></path>
  34. </svg>
  35. `;
  36. static {
  37. const css = new CSSStyleSheet();
  38. css.replaceSync(this.css);
  39. document.adoptedStyleSheets.push(css);
  40. }
  41. static get shouldLoad() {
  42. return 'Summarizer' in window;
  43. }
  44. /** A cached Summarizer instance Promise to avoid recreating it unnecessarily. */
  45. #summarizer = null;
  46. contentLanguage = document.documentElement.lang || 'en';
  47. /** Promise of a browser Summarizer instance. */
  48. get summarizer() {
  49. if (this.#summarizer) return this.#summarizer; // Return from cache
  50. const sharedContext =
  51. 'A summary of the content on a webpage, suitable for use as a meta description.';
  52. // eslint-disable-next-line no-undef
  53. this.#summarizer = Summarizer.create({
  54. sharedContext,
  55. type: this.typeValue,
  56. length: this.lengthValue,
  57. format: 'plain-text',
  58. expectedInputLanguages: [this.contentLanguage],
  59. outputLanguage: document.documentElement.lang,
  60. monitor: (m) => {
  61. m.addEventListener('downloadprogress', (event) => {
  62. const label = this.suggestLabel;
  63. const { loaded, total } = event;
  64. if (loaded === total) {
  65. label.textContent = 'Generating…';
  66. return;
  67. }
  68. const percent = Math.round((loaded / total) * 100);
  69. label.textContent = `Loading AI… ${percent}%`;
  70. });
  71. },
  72. });
  73. return this.#summarizer;
  74. }
  75. // Override only for JSDoc/typing purposes, not for functionality
  76. /** @returns {HTMLElement} */
  77. get element() {
  78. return super.element;
  79. }
  80. get suggestLabel() {
  81. return this.suggestTarget.lastElementChild;
  82. }
  83. connect() {
  84. this.generate = this.generate.bind(this);
  85. this.input = this.element.querySelector(this.inputValue);
  86. this.renderFurniture();
  87. }
  88. renderFurniture() {
  89. this.renderSuggestButton();
  90. this.renderOutputArea();
  91. }
  92. renderSuggestButton() {
  93. if (this.hasSuggestTarget) return;
  94. const prefix = this.element.closest('[id]').id;
  95. const buttonId = `${prefix}-generate`;
  96. const button = /* html */ `
  97. <button
  98. id="${buttonId}"
  99. type="button"
  100. data-summarize-target="suggest"
  101. data-action="summarize#generate"
  102. class="button"
  103. >
  104. ${SummarizeController.icon}
  105. <span>Generate suggestions</span>
  106. </button>
  107. <button
  108. type="button"
  109. data-summarize-target="clear"
  110. data-action="summarize#clearOutputArea"
  111. class="button button-secondary"
  112. hidden
  113. >
  114. Clear suggestions
  115. </button>
  116. `;
  117. this.element.insertAdjacentHTML('beforeend', button);
  118. }
  119. renderOutputArea() {
  120. this.outputArea = document.createElement('div');
  121. this.outputArea.classList.add('summarize-output');
  122. this.element.append(this.outputArea);
  123. }
  124. clearOutputArea() {
  125. this.outputArea.innerHTML = ''; // Clear previous output
  126. this.clearTarget.hidden = true; // Hide the clear button
  127. }
  128. renderSuggestion(suggestion) {
  129. const template = document.createElement('template');
  130. template.innerHTML = /* html */ `
  131. <div class="suggestion">
  132. <output for="${this.suggestTarget.id}">${suggestion}</output>
  133. <button class="button button-small" type="button" data-action="summarize#useSuggestion">Use</button>
  134. </div>
  135. `;
  136. this.outputArea.append(template.content.firstElementChild);
  137. }
  138. useSuggestion(event) {
  139. if (!this.input) return;
  140. this.input.value = event.target.previousElementSibling.textContent;
  141. this.input.dispatchEvent(new Event('input')); // Trigger autosize
  142. }
  143. async summarize(text) {
  144. const summarizer = await this.summarizer;
  145. return summarizer.summarize(text);
  146. }
  147. async getPageContent() {
  148. const previewController = window.wagtail.app.queryController('w-preview');
  149. const { innerText, lang } = await previewController.extractContent();
  150. this.contentLanguage = lang;
  151. return innerText;
  152. }
  153. async generate() {
  154. this.clearOutputArea();
  155. const label = this.suggestLabel;
  156. label.textContent = 'Generating…';
  157. this.suggestTarget.disabled = true;
  158. const text = await this.getPageContent();
  159. await Promise.allSettled(
  160. [...Array(3).keys()].map(() =>
  161. this.summarize(text)
  162. .then((output) => this.renderSuggestion(output))
  163. .catch((error) => {
  164. console.error('Error generating suggestion:', error);
  165. }),
  166. ),
  167. );
  168. this.suggestTarget.disabled = false;
  169. label.textContent = 'Generate suggestions';
  170. this.clearTarget.hidden = false; // Show the clear button
  171. }
  172. }
  173. window.wagtail.app.register('summarize', SummarizeController);