Skip to content

Commit 07560d0

Browse files
committed
Add meta description generator using browser Summarizer API
1 parent 9db64d0 commit 07560d0

3 files changed

Lines changed: 209 additions & 1 deletion

File tree

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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);

bakerydemo/base/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@
4141
# Allow filtering by collection
4242
Image.api_fields = [APIField("collection")]
4343

44+
# Replace the default `search_description` FieldPanel with a custom one
45+
# that uses the `summarize` controller for generating summaries.
46+
Page.promote_panels[0].args[0][-1] = FieldPanel(
47+
"search_description",
48+
attrs={
49+
"data-controller": "summarize",
50+
"data-summarize-input-value": "textarea[name='search_description']",
51+
},
52+
)
53+
4454

4555
class Person(
4656
WorkflowMixin,

bakerydemo/base/wagtail_hooks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ class BakerySnippetViewSetGroup(SnippetViewSetGroup):
9898

9999
@hooks.register("insert_editor_js")
100100
def editor_js():
101-
js_files = ["js/alt-text.js"]
101+
js_files = ["js/alt-text.js", "js/summarize.js"]
102102
return format_html_join(
103103
"\n",
104104
'<script src="{}"></script>',

0 commit comments

Comments
 (0)