class SummarizeController extends window.StimulusModule.Controller { static targets = ['suggest', 'clear']; static values = { input: { default: '', type: String }, type: { default: 'teaser', type: String }, length: { default: 'short', type: String }, }; static css = /* css */ ` .summarize-output { margin-top: 1rem; } .suggestion { display: block; margin-top: 0.5rem; margin-bottom: 0.5rem; border-radius: 0.25rem; padding: 0.5rem; background-color: lightblue; color: black; } `; static icon = /* html */ ` `; static { const css = new CSSStyleSheet(); css.replaceSync(this.css); document.adoptedStyleSheets.push(css); } static get shouldLoad() { return 'Summarizer' in window; } /** A cached Summarizer instance Promise to avoid recreating it unnecessarily. */ #summarizer = null; contentLanguage = document.documentElement.lang || 'en'; /** Promise of a browser Summarizer instance. */ get summarizer() { if (this.#summarizer) return this.#summarizer; // Return from cache const sharedContext = 'A summary of the content on a webpage, suitable for use as a meta description.'; // eslint-disable-next-line no-undef this.#summarizer = Summarizer.create({ sharedContext, type: this.typeValue, length: this.lengthValue, format: 'plain-text', expectedInputLanguages: [this.contentLanguage], outputLanguage: document.documentElement.lang, monitor: (m) => { m.addEventListener('downloadprogress', (event) => { const label = this.suggestLabel; const { loaded, total } = event; if (loaded === total) { label.textContent = 'Generating…'; return; } const percent = Math.round((loaded / total) * 100); label.textContent = `Loading AI… ${percent}%`; }); }, }); return this.#summarizer; } // Override only for JSDoc/typing purposes, not for functionality /** @returns {HTMLElement} */ get element() { return super.element; } get suggestLabel() { return this.suggestTarget.lastElementChild; } connect() { this.generate = this.generate.bind(this); this.input = this.element.querySelector(this.inputValue); this.renderFurniture(); } renderFurniture() { this.renderSuggestButton(); this.renderOutputArea(); } renderSuggestButton() { if (this.hasSuggestTarget) return; const prefix = this.element.closest('[id]').id; const buttonId = `${prefix}-generate`; const button = /* html */ ` `; this.element.insertAdjacentHTML('beforeend', button); } renderOutputArea() { this.outputArea = document.createElement('div'); this.outputArea.classList.add('summarize-output'); this.element.append(this.outputArea); } clearOutputArea() { this.outputArea.innerHTML = ''; // Clear previous output this.clearTarget.hidden = true; // Hide the clear button } renderSuggestion(suggestion) { const template = document.createElement('template'); template.innerHTML = /* html */ `
${suggestion}
`; this.outputArea.append(template.content.firstElementChild); } useSuggestion(event) { if (!this.input) return; this.input.value = event.target.previousElementSibling.textContent; this.input.dispatchEvent(new Event('input')); // Trigger autosize } async summarize(text) { const summarizer = await this.summarizer; return summarizer.summarize(text); } async getPageContent() { const previewController = window.wagtail.app.queryController('w-preview'); const { innerText, lang } = await previewController.extractContent(); this.contentLanguage = lang; return innerText; } async generate() { this.clearOutputArea(); const label = this.suggestLabel; label.textContent = 'Generating…'; this.suggestTarget.disabled = true; const text = await this.getPageContent(); await Promise.allSettled( [...Array(3).keys()].map(() => this.summarize(text) .then((output) => this.renderSuggestion(output)) .catch((error) => { console.error('Error generating suggestion:', error); }), ), ); this.suggestTarget.disabled = false; label.textContent = 'Generate suggestions'; this.clearTarget.hidden = false; // Show the clear button } } window.wagtail.app.register('summarize', SummarizeController);