contextual-alt.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. 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>`;
  2. class ContextualAltController extends window.StimulusModule.Controller {
  3. static targets = ['suggest'];
  4. static values = {
  5. imageInput: { default: '', type: String },
  6. captionInput: { default: '', type: String },
  7. };
  8. /** An image-to-text pipeline, shared between all instances of this controller. */
  9. static captioner;
  10. static {
  11. this.captioner = import(
  12. 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2'
  13. ).then(({ pipeline }) => pipeline('image-to-text', 'Mozilla/distilvit'));
  14. }
  15. get imageURL() {
  16. return this.element.querySelector('img[data-chooser-image]')?.src || '';
  17. }
  18. /** @returns {HTMLElement} */
  19. get element() {
  20. return super.element;
  21. }
  22. connect() {
  23. this.summarize = this.summarize.bind(this);
  24. this.renderFurniture();
  25. }
  26. imageInputValueChanged() {
  27. if (this.imageInputValue) {
  28. this.imageInput = this.element.querySelector(this.imageInputValue);
  29. } else {
  30. this.imageInput = null;
  31. }
  32. if (this.hasSuggestTarget) this.toggleSuggestTarget();
  33. }
  34. captionInputValueChanged() {
  35. if (this.captionInputValue) {
  36. this.captionInput = this.element.querySelector(this.captionInputValue);
  37. } else {
  38. this.captionInput = null;
  39. }
  40. }
  41. toggleSuggestTarget(event) {
  42. if (event?.target && event.target !== this.imageInput) return;
  43. this.suggestTarget.disabled = !this.imageInput?.value;
  44. }
  45. renderFurniture() {
  46. this.renderSuggestButton();
  47. this.renderOutputArea();
  48. this.toggleSuggestTarget();
  49. }
  50. renderSuggestButton() {
  51. if (this.hasSuggestTarget) return;
  52. const prefix = this.element.closest('[id]').id;
  53. const buttonId = `${prefix}-generate`;
  54. const button = /* html */ `
  55. <button
  56. id="${buttonId}"
  57. type="button"
  58. data-contextual-alt-target="suggest"
  59. data-action="contextual-alt#summarize"
  60. class="button button-secondary"
  61. >
  62. ${icon}
  63. <span>Generate suggestions</span>
  64. </button>
  65. `;
  66. this.element.insertAdjacentHTML('beforeend', button);
  67. }
  68. renderOutputArea() {
  69. const css = new CSSStyleSheet();
  70. css.replaceSync(/* css */ `
  71. .suggestion {
  72. display: block;
  73. margin-top: 0.5rem;
  74. margin-bottom: 0.5rem;
  75. border-radius: 0.25rem;
  76. padding: 0.5rem;
  77. background-color: lightblue;
  78. color: black;
  79. }
  80. `);
  81. this.outputArea = document.createElement('div');
  82. document.adoptedStyleSheets.push(css);
  83. this.element.append(this.outputArea);
  84. }
  85. renderSuggestion(suggestion) {
  86. const template = document.createElement('template');
  87. template.innerHTML = /* html */ `
  88. <div for="${this.suggestTarget.id}" class="suggestion">
  89. <output>${suggestion}</output>
  90. <button class="button button-small" type="button" data-action="contextual-alt#useSuggestion">Use</button>
  91. </div>
  92. `;
  93. this.outputArea.append(template.content.firstElementChild);
  94. }
  95. useSuggestion(event) {
  96. if (!this.captionInput) return;
  97. console.log(event.target);
  98. this.captionInput.value = event.target.previousElementSibling.textContent;
  99. }
  100. async caption(imageURL) {
  101. const output = await (await ContextualAltController.captioner)(imageURL);
  102. return output[0].generated_text;
  103. }
  104. async summarize() {
  105. this.outputArea.innerHTML = ''; // Clear previous output
  106. this.suggestTarget.lastElementChild.textContent = 'Generating…';
  107. this.suggestTarget.disabled = true;
  108. const url = this.imageURL;
  109. await Promise.allSettled(
  110. [...Array(3).keys()].map(() =>
  111. this.caption(url).then((output) => {
  112. this.renderSuggestion(output);
  113. }),
  114. ),
  115. );
  116. this.suggestTarget.disabled = false;
  117. this.suggestTarget.lastElementChild.textContent = 'Generate suggestions';
  118. }
  119. disconnect() {
  120. ContextualAltController.captioner.then((captioner) => captioner.dispose());
  121. }
  122. }
  123. window.wagtail.app.register('contextual-alt', ContextualAltController);