PluginProbe ʕ •ᴥ•ʔ
AI Agent by SiteGround / 1.1.5
AI Agent by SiteGround v1.1.5
1.2.5 1.2.4 1.2.3 1.2.2 1.2.1 1.2.0 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 trunk 1.1.3
sg-ai-studio / assets / blocks / aistudio-prompt / index.js
sg-ai-studio / assets / blocks / aistudio-prompt Last commit date
block.json 2 months ago index.css 2 months ago index.js 2 months ago render.php 2 months ago
index.js
526 lines
1 /**
2 * SG AI Studio - AI Studio Prompt Block
3 *
4 * A Gutenberg block for generating content using AI Studio.
5 */
6
7 (function(wp) {
8 var registerBlockType = wp.blocks.registerBlockType;
9 var Button = wp.components.Button;
10 var ExternalLink = wp.components.ExternalLink;
11 var createElement = wp.element.createElement;
12 var Fragment = wp.element.Fragment;
13
14 // Fix for i18n
15 var __ = wp.i18n.__ || function(text) { return text; };
16
17 /**
18 * Register the block
19 */
20 registerBlockType('sg-ai-studio/ai-studio-prompt', {
21 title: 'AI Content Generator',
22 description: 'Generate content using AI Studio',
23 category: 'text',
24 icon: 'editor-paste-text',
25 keywords: ['ai', 'aistudio', 'content', 'generator'],
26 apiVersion: 3,
27 attributes: {
28 prompt: {
29 type: 'string',
30 default: ''
31 },
32 isGenerating: {
33 type: 'boolean',
34 default: false
35 }
36 },
37
38 /**
39 * Edit function for the block
40 */
41 edit: function(props) {
42 var attributes = props.attributes;
43 var setAttributes = props.setAttributes;
44 var prompt = attributes.prompt;
45 var isGenerating = attributes.isGenerating;
46
47 // Get API key status from global variable
48 var hasApiKey = typeof sgAiStudioBlock !== 'undefined' && sgAiStudioBlock.hasApiKey;
49
50 /**
51 * Generate content from AI Studio
52 */
53 function generateContent() {
54 if (!prompt) {
55 return;
56 }
57
58 // Set the generating state
59 setAttributes({ isGenerating: true });
60
61 // Get the current post ID
62 var postId = wp.data.select('core/editor').getCurrentPostId();
63
64 // Call AI Studio via WordPress REST API
65 wp.apiFetch({
66 path: '/sg-ai-studio/generate-content',
67 method: 'POST',
68 data: {
69 prompt: prompt,
70 nonce: sgAiStudioBlock.nonce,
71 post_id: postId
72 }
73 })
74 .then(function(response) {
75 if (response.success && (response.data || response.images)) {
76 // Insert content into the editor
77 insertContentIntoEditor(response.data, response.images);
78 } else {
79 // Store error in local variable for React to use in rendering
80 props.error = response.message || 'Error generating content.';
81 // Force re-render
82 setAttributes({ isGenerating: false });
83 }
84 })
85 .catch(function(error) {
86 console.error('Error generating content:', error);
87 // Store error in local variable for React to use in rendering
88 props.error = 'Error connecting to the AI service.';
89 // Force re-render
90 setAttributes({ isGenerating: false });
91 })
92 .finally(function() {
93 setAttributes({ isGenerating: false });
94 });
95 }
96
97 /**
98 * Insert content into the editor
99 */
100 function insertContentIntoEditor(content, images) {
101 if (!content && !images) {
102 return;
103 }
104
105 var blocks = [];
106
107 // Process text content if available
108 if (content) {
109 // Handle potential escaped HTML entities
110 content = content.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
111
112 // Check if content has an H1 heading to use as post title
113 // Support both <h1> tags and potential escaped versions
114 var titleRegex = /<h1[^>]*>(.*?)<\/h1>/i;
115 var titleMatch = content.match(titleRegex);
116
117 // Extract the title from H1 if it exists
118 if (titleMatch && titleMatch[1]) {
119 var title = titleMatch[1].trim();
120 // Strip any HTML tags from the title
121 var tempDiv = document.createElement('div');
122 tempDiv.innerHTML = title;
123 title = tempDiv.textContent || tempDiv.innerText || title;
124
125 // Set as post title
126 wp.data.dispatch('core/editor').editPost({ title: title });
127
128 // Remove the H1 from content before processing
129 content = content.replace(titleRegex, '').trim();
130 }
131
132 // Check if content contains Gutenberg block comments
133 var hasBlockComments = content.indexOf('<!-- wp:') !== -1;
134
135 if (hasBlockComments) {
136 // Parse Gutenberg block markup
137 blocks = wp.blocks.parse(content);
138 } else {
139 // Use rawHandler to convert plain HTML to blocks
140 blocks = wp.blocks.rawHandler({
141 HTML: content,
142 mode: 'BLOCKS'
143 });
144 }
145
146 // If parsing failed, fallback to paragraph
147 if (!blocks || blocks.length === 0) {
148 blocks = [wp.blocks.createBlock('core/paragraph', {
149 content: content
150 })];
151 }
152 }
153
154 // Add image blocks if images are provided separately (not already in content)
155 // Check if images are already embedded in the parsed blocks
156 if (images && images.length > 0) {
157 // Recursive function to check for image blocks at any nesting level
158 function hasImageBlocksRecursive(blockList) {
159 for (var j = 0; j < blockList.length; j++) {
160 var block = blockList[j];
161 // Check for image, cover, gallery, or media-text blocks
162 if (block.name === 'core/image' ||
163 block.name === 'core/cover' ||
164 block.name === 'core/gallery' ||
165 block.name === 'core/media-text') {
166 return true;
167 }
168 // Check inner blocks recursively
169 if (block.innerBlocks && block.innerBlocks.length > 0) {
170 if (hasImageBlocksRecursive(block.innerBlocks)) {
171 return true;
172 }
173 }
174 }
175 return false;
176 }
177
178 var hasImageBlocks = hasImageBlocksRecursive(blocks);
179
180 // Only add image blocks if they weren't already included in the parsed content
181 if (!hasImageBlocks) {
182 for (var i = 0; i < images.length; i++) {
183 var imageBlock = wp.blocks.createBlock('core/image', {
184 id: images[i].id,
185 url: images[i].url,
186 sizeSlug: 'large'
187 });
188 blocks.push(imageBlock);
189 }
190 }
191 }
192
193 // Replace temporary/placeholder image URLs with actual WordPress URLs if images array is provided
194 if (images && images.length > 0) {
195 var imageIndex = 0;
196
197 // Helper function to check if URL needs replacement
198 function needsUrlReplacement(url) {
199 if (!url) return false;
200 // Check for common placeholder patterns first
201 if (url.indexOf('/internal/res_') !== -1) return true;
202 if (url.indexOf('localhost/internal') !== -1) return true;
203 if (url.indexOf('placeholder') !== -1) return true;
204 if (url.indexOf('temp') !== -1) return true;
205 // Check if it's an external URL (not from WordPress uploads)
206 if (url.indexOf('http') === 0 && url.indexOf(window.location.hostname) === -1) return true;
207 return false;
208 }
209
210 // Recursive function to update image URLs in blocks
211 function replaceImageUrls(blockList) {
212 for (var k = 0; k < blockList.length; k++) {
213 var block = blockList[k];
214
215 // Update media-text blocks - remove nested image blocks and use mediaUrl instead
216 if (block.name === 'core/media-text' && block.attributes) {
217 var needsMediaUpdate = false;
218 var contentBlocks = []; // To store non-image inner blocks
219
220 // Check if mediaUrl needs replacement
221 if (block.attributes.mediaUrl && needsUrlReplacement(block.attributes.mediaUrl)) {
222 needsMediaUpdate = true;
223 }
224 // Check for mediaLink attribute (non-standard but sometimes used)
225 else if (block.attributes.mediaLink && needsUrlReplacement(block.attributes.mediaLink)) {
226 needsMediaUpdate = true;
227 }
228 // Check for non-standard image_url attribute
229 else if (block.attributes.image_url && needsUrlReplacement(block.attributes.image_url)) {
230 needsMediaUpdate = true;
231 }
232 // Check if mediaType is image but no mediaUrl is set yet
233 else if (block.attributes.mediaType === 'image' && !block.attributes.mediaUrl) {
234 needsMediaUpdate = true;
235 }
236
237 // Look for nested image block to extract URL and separate content blocks
238 if (block.innerBlocks && block.innerBlocks.length > 0) {
239 for (var m = 0; m < block.innerBlocks.length; m++) {
240 var innerBlock = block.innerBlocks[m];
241 // If it's an image block, extract its URL and don't keep it
242 if (innerBlock.name === 'core/image' && innerBlock.attributes && innerBlock.attributes.url) {
243 if (needsUrlReplacement(innerBlock.attributes.url)) {
244 needsMediaUpdate = true;
245 }
246 // Don't add image block to contentBlocks - we'll use mediaUrl instead
247 } else {
248 // Keep other inner blocks (paragraphs, headings, etc.)
249 contentBlocks.push(innerBlock);
250 }
251 }
252 }
253
254 // Create fresh block instead of modifying existing one
255 if (needsMediaUpdate && imageIndex < images.length) {
256
257 // Create new attributes object with correct values
258 var newAttributes = {};
259 for (var attrKey in block.attributes) {
260 if (block.attributes.hasOwnProperty(attrKey)) {
261 newAttributes[attrKey] = block.attributes[attrKey];
262 }
263 }
264
265 // Set correct media attributes
266 newAttributes.mediaUrl = images[imageIndex].url;
267 newAttributes.mediaId = images[imageIndex].id;
268 newAttributes.mediaType = 'image';
269
270 // Clean up non-standard attributes
271 delete newAttributes.image_url;
272 delete newAttributes.image_id;
273 delete newAttributes.mediaLink;
274
275 // Create fresh block with cleaned content blocks
276 var newBlock = wp.blocks.createBlock('core/media-text', newAttributes, contentBlocks);
277
278 // Replace in array
279 blockList[k] = newBlock;
280
281 imageIndex++;
282 } else if (needsMediaUpdate) {
283 } else {
284 // No media update needed, but still process inner blocks
285 if (block.innerBlocks && block.innerBlocks.length > 0) {
286 replaceImageUrls(block.innerBlocks);
287 }
288 }
289 }
290 // Update standalone image blocks
291 else if (block.name === 'core/image' && block.attributes && block.attributes.url) {
292 if (needsUrlReplacement(block.attributes.url)) {
293 if (imageIndex < images.length) {
294 block.attributes.url = images[imageIndex].url;
295 block.attributes.id = images[imageIndex].id;
296 imageIndex++;
297 }
298 }
299 }
300 // Update cover blocks
301 else if (block.name === 'core/cover' && block.attributes && block.attributes.url) {
302 if (needsUrlReplacement(block.attributes.url)) {
303 if (imageIndex < images.length) {
304 block.attributes.url = images[imageIndex].url;
305 block.attributes.id = images[imageIndex].id;
306 imageIndex++;
307 }
308 }
309
310 // Process inner blocks for cover
311 if (block.innerBlocks && block.innerBlocks.length > 0) {
312 replaceImageUrls(block.innerBlocks);
313 }
314 }
315 // Recursively update inner blocks for other block types
316 else if (block.innerBlocks && block.innerBlocks.length > 0) {
317 replaceImageUrls(block.innerBlocks);
318 }
319 }
320 }
321
322 replaceImageUrls(blocks);
323 }
324
325 // Get the current block's position info
326 var selectedBlockId = wp.data.select('core/block-editor').getSelectedBlockClientId();
327
328 if (selectedBlockId) {
329 // Get the index and parent of the current block
330 var blockIndex = wp.data.select('core/block-editor').getBlockIndex(selectedBlockId);
331 var rootClientId = wp.data.select('core/block-editor').getBlockRootClientId(selectedBlockId);
332
333 // Insert the new blocks at the same position
334 wp.data.dispatch('core/block-editor').insertBlocks(blocks, blockIndex, rootClientId);
335
336 // Remove the AI Studio Prompt block (which is now at blockIndex + blocks.length)
337 wp.data.dispatch('core/block-editor').removeBlock(selectedBlockId);
338 } else {
339 // Fallback: insert at the end
340 wp.data.dispatch('core/block-editor').insertBlocks(blocks);
341 }
342
343 wp.data.dispatch('core/editor').savePost();
344 }
345
346 /**
347 * Render the block editor UI
348 */
349 return createElement(
350 'div',
351 {
352 className: 'wp-block-sg-ai-studio-prompt',
353 style: {
354 padding: '16px',
355 position: 'relative',
356 background: '#fff',
357 border: '1px solid #e0e0e0',
358 borderRadius: '4px'
359 }
360 },
361 [
362 // Loading spinner overlay when generating
363 isGenerating && createElement('div', {
364 style: {
365 position: 'absolute',
366 top: '0',
367 left: '0',
368 right: '0',
369 bottom: '0',
370 background: 'rgba(255, 255, 255, 0.8)',
371 backdropFilter: 'blur(2px)',
372 display: 'flex',
373 alignItems: 'center',
374 justifyContent: 'center',
375 zIndex: 1000,
376 pointerEvents: 'all',
377 borderRadius: '4px'
378 }
379 },
380 createElement('div', {
381 style: {
382 width: '40px',
383 height: '40px',
384 border: '4px solid #e0e0e0',
385 borderTop: '4px solid #6366f1',
386 borderRadius: '50%',
387 animation: 'spin 1s linear infinite'
388 }
389 })
390 ),
391
392 // API Key notice
393 !hasApiKey && createElement(
394 'div',
395 {
396 className: 'sg-ai-studio-settings-notice',
397 style: {
398 padding: '12px',
399 background: '#fff3cd',
400 border: '1px solid #ffc107',
401 borderRadius: '4px',
402 marginBottom: '16px',
403 display: 'flex',
404 alignItems: 'center',
405 gap: '8px'
406 }
407 },
408 [
409 createElement('span', { style: { flex: 1 } }, 'Please configure your AI Studio API key in the plugin settings.'),
410 createElement(ExternalLink, { href: sgAiStudioBlock.settingsUrl }, 'Settings')
411 ]
412 ),
413
414 // Prompt container with styled text section
415 createElement(
416 'div',
417 { className: 'sg-ai-studio-text-section', style: { marginBottom: '16px' } },
418 [
419 createElement('label', {
420 className: 'components-base-control__label',
421 style: { display: 'block', marginBottom: '8px', fontWeight: '600' }
422 }, 'Generate Content with AI'),
423 createElement('textarea', {
424 className: 'sg-ai-studio-text-input',
425 value: prompt,
426 onChange: function(e) {
427 setAttributes({ prompt: e.target.value });
428 // Clear any previous error when prompt changes
429 props.error = null;
430 },
431 placeholder: 'Describe the content you want to generate...',
432 disabled: isGenerating || !hasApiKey,
433 rows: 6,
434 style: {
435 width: '100%',
436 padding: '12px',
437 background: isGenerating ? '#f9f9f9' : '#f0f7ff',
438 border: isGenerating ? '2px solid #6164ff' : '1px solid var(--color-primary-main, #4343f0)',
439 borderRadius: '4px',
440 fontSize: '14px',
441 lineHeight: '1.6',
442 fontFamily: 'inherit',
443 resize: 'vertical',
444 minHeight: '120px',
445 boxSizing: 'border-box',
446 outline: 'none'
447 }
448 })
449 ]
450 ),
451
452 // Error message (if any)
453 props.error && createElement(
454 'div',
455 {
456 className: 'sg-ai-studio-error',
457 style: {
458 padding: '12px',
459 background: '#f8d7da',
460 border: '1px solid #f5c2c7',
461 borderRadius: '4px',
462 color: '#842029',
463 marginBottom: '16px',
464 fontSize: '14px'
465 }
466 },
467 props.error
468 ),
469
470 // Button container
471 createElement(
472 'div',
473 { className: 'sg-ai-studio-button-container', style: { marginBottom: '16px' } },
474 [
475 createElement(
476 Button,
477 {
478 className: 'sg-button-base sg-button-base--color-primary sg-button-base--type-filled sg-button sg-button--medium',
479 isPrimary: true,
480 onClick: generateContent,
481 disabled: isGenerating || !prompt || !hasApiKey,
482 style: {
483 width: '100%',
484 justifyContent: 'center',
485 background: 'var(--color-primary-main, #4343f0)',
486 color: 'var(--color-primary-contrast, #ffffff)',
487 border: 'none',
488 borderRadius: '50px',
489 fontFamily: 'Poppins, -apple-system, BlinkMacSystemFont, sans-serif',
490 fontSize: 'var(--typography-size-medium, 1.4rem)',
491 fontWeight: 'var(--typography-weight-medium, 500)',
492 padding: 'var(--space-x-small, 8px) var(--space-small, 12px)',
493 height: 'auto',
494 minHeight: '40px'
495 }
496 },
497 isGenerating ? 'Generating...' : 'Generate'
498 ),
499 createElement(
500 'p',
501 {
502 className: 'sg-ai-studio-info',
503 style: {
504 fontSize: '12px',
505 color: '#757575',
506 textAlign: 'center',
507 margin: '8px 0 0 0'
508 }
509 },
510 'Generated content will be added to your post.'
511 )
512 ]
513 )
514 ].filter(Boolean) // Filter out any falsy elements (like when !hasApiKey is false)
515 );
516 },
517
518 /**
519 * Save function for the block
520 */
521 save: function() {
522 // This block is dynamic and doesn't save content
523 return null;
524 }
525 });
526 })(window.wp);