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