|
@@ -0,0 +1,143 @@
|
|
|
+const icon = `<svg width="16" height="16" class="Draftail-Icon" aria-hidden="true" viewBox="0 0 576 512" fill="currentColor"><path 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"></path></svg>`;
|
|
|
+
|
|
|
+class ContextualAltController extends window.StimulusModule.Controller {
|
|
|
+ static targets = ['suggest'];
|
|
|
+ static values = {
|
|
|
+ imageInput: { default: '', type: String },
|
|
|
+ captionInput: { default: '', type: String },
|
|
|
+ };
|
|
|
+
|
|
|
+ /** An image-to-text pipeline, shared between all instances of this controller. */
|
|
|
+ static captioner;
|
|
|
+ static {
|
|
|
+ this.captioner = import(
|
|
|
+ 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2'
|
|
|
+ ).then(({ pipeline }) => pipeline('image-to-text', 'Mozilla/distilvit'));
|
|
|
+ }
|
|
|
+
|
|
|
+ get imageURL() {
|
|
|
+ return this.element.querySelector('img[data-chooser-image]')?.src || '';
|
|
|
+ }
|
|
|
+
|
|
|
+ /** @returns {HTMLElement} */
|
|
|
+ get element() {
|
|
|
+ return super.element;
|
|
|
+ }
|
|
|
+
|
|
|
+ connect() {
|
|
|
+ this.summarize = this.summarize.bind(this);
|
|
|
+ this.renderFurniture();
|
|
|
+ }
|
|
|
+
|
|
|
+ imageInputValueChanged() {
|
|
|
+ if (this.imageInputValue) {
|
|
|
+ this.imageInput = this.element.querySelector(this.imageInputValue);
|
|
|
+ } else {
|
|
|
+ this.imageInput = null;
|
|
|
+ }
|
|
|
+ if (this.hasSuggestTarget) this.toggleSuggestTarget();
|
|
|
+ }
|
|
|
+
|
|
|
+ captionInputValueChanged() {
|
|
|
+ if (this.captionInputValue) {
|
|
|
+ this.captionInput = this.element.querySelector(this.captionInputValue);
|
|
|
+ } else {
|
|
|
+ this.captionInput = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ toggleSuggestTarget(event) {
|
|
|
+ if (event?.target && event.target !== this.imageInput) return;
|
|
|
+ this.suggestTarget.disabled = !this.imageInput?.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ renderFurniture() {
|
|
|
+ this.renderSuggestButton();
|
|
|
+ this.renderOutputArea();
|
|
|
+ this.toggleSuggestTarget();
|
|
|
+ }
|
|
|
+
|
|
|
+ renderSuggestButton() {
|
|
|
+ if (this.hasSuggestTarget) return;
|
|
|
+ const prefix = this.element.closest('[id]').id;
|
|
|
+ const buttonId = `${prefix}-generate`;
|
|
|
+ const button = /* html */ `
|
|
|
+ <button
|
|
|
+ id="${buttonId}"
|
|
|
+ type="button"
|
|
|
+ data-contextual-alt-target="suggest"
|
|
|
+ data-action="contextual-alt#summarize"
|
|
|
+ class="button button-secondary"
|
|
|
+ >
|
|
|
+ ${icon}
|
|
|
+
|
|
|
+ <span>Generate suggestions</span>
|
|
|
+ </button>
|
|
|
+ `;
|
|
|
+ this.element.insertAdjacentHTML('beforeend', button);
|
|
|
+ }
|
|
|
+
|
|
|
+ renderOutputArea() {
|
|
|
+ const css = new CSSStyleSheet();
|
|
|
+ css.replaceSync(/* css */ `
|
|
|
+ .suggestion {
|
|
|
+ display: block;
|
|
|
+ margin-top: 0.5rem;
|
|
|
+ margin-bottom: 0.5rem;
|
|
|
+ border-radius: 0.25rem;
|
|
|
+ padding: 0.5rem;
|
|
|
+ background-color: lightblue;
|
|
|
+ color: black;
|
|
|
+ }
|
|
|
+ `);
|
|
|
+ this.outputArea = document.createElement('div');
|
|
|
+ document.adoptedStyleSheets.push(css);
|
|
|
+ this.element.append(this.outputArea);
|
|
|
+ }
|
|
|
+
|
|
|
+ renderSuggestion(suggestion) {
|
|
|
+ const template = document.createElement('template');
|
|
|
+ template.innerHTML = /* html */ `
|
|
|
+ <div for="${this.suggestTarget.id}" class="suggestion">
|
|
|
+ <output>${suggestion}</output>
|
|
|
+ <button class="button button-small" type="button" data-action="contextual-alt#useSuggestion">Use</button>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ this.outputArea.append(template.content.firstElementChild);
|
|
|
+ }
|
|
|
+
|
|
|
+ useSuggestion(event) {
|
|
|
+ if (!this.captionInput) return;
|
|
|
+ console.log(event.target);
|
|
|
+ this.captionInput.value = event.target.previousElementSibling.textContent;
|
|
|
+ }
|
|
|
+
|
|
|
+ async caption(imageURL) {
|
|
|
+ const output = await (await ContextualAltController.captioner)(imageURL);
|
|
|
+ return output[0].generated_text;
|
|
|
+ }
|
|
|
+
|
|
|
+ async summarize() {
|
|
|
+ this.outputArea.innerHTML = ''; // Clear previous output
|
|
|
+ this.suggestTarget.lastElementChild.textContent = 'Generating…';
|
|
|
+ this.suggestTarget.disabled = true;
|
|
|
+
|
|
|
+ const url = this.imageURL;
|
|
|
+ await Promise.allSettled(
|
|
|
+ [...Array(3).keys()].map(() =>
|
|
|
+ this.caption(url).then((output) => {
|
|
|
+ this.renderSuggestion(output);
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+
|
|
|
+ this.suggestTarget.disabled = false;
|
|
|
+ this.suggestTarget.lastElementChild.textContent = 'Generate suggestions';
|
|
|
+ }
|
|
|
+
|
|
|
+ disconnect() {
|
|
|
+ ContextualAltController.captioner.then((captioner) => captioner.dispose());
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+window.wagtail.app.register('contextual-alt', ContextualAltController);
|