| @@ -1,6 +1,8 @@ | ||
| 1 | 1 | import { AI_HOST } from '@constants'; |
| 2 | +import { renderAiLabelPill, stampAiLabel } from '@shared/lib/ai-label'; | |
| 2 | 3 | import apiFetch from '@wordpress/api-fetch'; |
| 4 | +import { sprintf } from '@wordpress/i18n'; | |
| 3 | 5 | import { addQueryArgs } from '@wordpress/url'; |
| 4 | 6 | |
| 5 | 7 | export const getPlugin = async (slug) => { |
| 6 | 8 | const response = await apiFetch({ |
| @@ -25,10 +27,23 @@ | ||
| 25 | 27 | |
| 26 | 28 | return response; |
| 27 | 29 | }; |
| 28 | 30 | |
| 31 | +export const enableAutoUpdate = async (plugin) => { | |
| 32 | + if (!plugin) return; | |
| 33 | + try { | |
| 34 | + await apiFetch({ | |
| 35 | + path: '/extendify/v1/shared/enable-auto-update', | |
| 36 | + method: 'POST', | |
| 37 | + data: { plugin }, | |
| 38 | + }); | |
| 39 | + } catch (_e) { | |
| 40 | + // Best-effort: the install already succeeded. | |
| 41 | + } | |
| 42 | +}; | |
| 43 | + | |
| 29 | 44 | export const installPlugin = async (slug) => { |
| 30 | - return await apiFetch({ | |
| 45 | + const plugin = await apiFetch({ | |
| 31 | 46 | path: '/wp/v2/plugins', |
| 32 | 47 | method: 'POST', |
| 33 | 48 | data: { |
| 34 | 49 | slug, |
| @@ -33,8 +48,12 @@ | ||
| 33 | 48 | data: { |
| 34 | 49 | slug, |
| 35 | 50 | }, |
| 36 | 51 | }); |
| 52 | + | |
| 53 | + await enableAutoUpdate(plugin?.plugin); | |
| 54 | + | |
| 55 | + return plugin; | |
| 37 | 56 | }; |
| 38 | 57 | |
| 39 | 58 | export const activatePlugin = async (slug) => { |
| 40 | 59 | const plugin = await getPlugin(slug); |
| @@ -54,8 +73,17 @@ | ||
| 54 | 73 | img.onerror = (e) => reject(e); |
| 55 | 74 | }); |
| 56 | 75 | }; |
| 57 | 76 | |
| 77 | +const shouldDisclose = (metadata) => | |
| 78 | + Boolean(metadata.aiGenerated) && Boolean(metadata.disclose); | |
| 79 | + | |
| 80 | +const altText = (metadata) => { | |
| 81 | + if (!shouldDisclose(metadata)) return metadata.alt ?? ''; | |
| 82 | + const pattern = window.extSharedData?.aiImageAltPattern ?? 'AI Generated: %s'; | |
| 83 | + return sprintf(pattern, metadata.alt ?? '').trim(); | |
| 84 | +}; | |
| 85 | + | |
| 58 | 86 | export const importImage = async (imageUrl, metadata = {}) => { |
| 59 | 87 | const image = new Image(); |
| 60 | 88 | image.src = imageUrl; |
| 61 | 89 | image.crossOrigin = 'anonymous'; |
| @@ -67,8 +95,11 @@ | ||
| 67 | 95 | |
| 68 | 96 | const ctx = canvas.getContext('2d'); |
| 69 | 97 | if (!ctx) return; |
| 70 | 98 | ctx.drawImage(image, 0, 0); |
| 99 | + if (shouldDisclose(metadata)) { | |
| 100 | + stampAiLabel(ctx, canvas.width, canvas.height); | |
| 101 | + } | |
| 71 | 102 | |
| 72 | 103 | const blob = await new Promise((resolve) => { |
| 73 | 104 | canvas.toBlob((blob) => { |
| 74 | 105 | blob && resolve(blob); |
| @@ -76,11 +107,14 @@ | ||
| 76 | 107 | }); |
| 77 | 108 | |
| 78 | 109 | const formData = new FormData(); |
| 79 | 110 | formData.append('file', new File([blob], metadata.filename)); |
| 80 | - formData.append('alt_text', metadata.alt ?? ''); | |
| 111 | + formData.append('alt_text', altText(metadata)); | |
| 81 | 112 | formData.append('caption', metadata.caption ?? ''); |
| 82 | 113 | formData.append('status', 'publish'); |
| 114 | + if (metadata.aiGenerated) { | |
| 115 | + formData.append('meta[extendify_ai_generated]', '1'); | |
| 116 | + } | |
| 83 | 117 | |
| 84 | 118 | return await apiFetch({ |
| 85 | 119 | path: 'wp/v2/media', |
| 86 | 120 | method: 'POST', |
| @@ -91,10 +125,22 @@ | ||
| 91 | 125 | export const importImageServer = async (src, metadata = {}) => { |
| 92 | 126 | const formData = new FormData(); |
| 93 | 127 | formData.append('source', src); |
| 94 | 128 | // Fallback doesn't support custom file_name |
| 95 | - formData.append('alt_text', metadata.alt ?? ''); | |
| 129 | + formData.append('alt_text', altText(metadata)); | |
| 96 | 130 | formData.append('caption', metadata.caption ?? ''); |
| 131 | + if (metadata.aiGenerated) { | |
| 132 | + formData.append('ai_generated', '1'); | |
| 133 | + if (shouldDisclose(metadata)) { | |
| 134 | + const pill = await renderAiLabelPill(); | |
| 135 | + if (pill) { | |
| 136 | + formData.append( | |
| 137 | + 'disclosure_label', | |
| 138 | + new File([pill], 'disclosure-label.png', { type: 'image/png' }), | |
| 139 | + ); | |
| 140 | + } | |
| 141 | + } | |
| 142 | + } | |
| 97 | 143 | |
| 98 | 144 | return await apiFetch({ |
| 99 | 145 | path: '/extendify/v1/draft/upload-image', |
| 100 | 146 | method: 'POST', |
| @@ -109,22 +155,28 @@ | ||
| 109 | 155 | unsplashId, |
| 110 | 156 | metadata = { alt: '', caption: '' }, |
| 111 | 157 | ) => { |
| 112 | 158 | let image; |
| 159 | + const aiGenerated = source === 'ai-generated'; | |
| 113 | 160 | if (unsplashId) { |
| 114 | 161 | await downloadPing(id, source, { unsplashId }); |
| 162 | + } else if (aiGenerated && id) { | |
| 163 | + await downloadPing(id, source, { disclose: metadata.disclose }); | |
| 115 | 164 | } |
| 116 | 165 | try { |
| 117 | 166 | image = await importImage(src, { |
| 118 | 167 | alt: metadata.alt, |
| 119 | - filename: 'image.jpg', | |
| 168 | + filename: metadata.filename ?? 'image.jpg', | |
| 120 | 169 | caption: metadata.caption, |
| 170 | + aiGenerated, | |
| 171 | + disclose: metadata.disclose, | |
| 121 | 172 | }); |
| 122 | 173 | } catch (_e) { |
| 123 | 174 | image = await importImageServer(src, { |
| 124 | 175 | alt: metadata.alt, |
| 125 | - filename: 'image.jpg', | |
| 126 | 176 | caption: metadata.caption, |
| 177 | + aiGenerated, | |
| 178 | + disclose: metadata.disclose, | |
| 127 | 179 | }); |
| 128 | 180 | } |
| 129 | 181 | |
| 130 | 182 | return image; |