Changeset 3434643
- Timestamp:
- 01/07/2026 07:20:17 PM (3 months ago)
- Location:
- technodrome-ai-content-assistant/trunk
- Files:
-
- 3 added
- 12 edited
-
CHANGELOG.md (added)
-
dashboard/modules/generate-tab/generate.php (modified) (2 diffs)
-
dashboard/modules/layout-templates-tab/layout-templates.php (modified) (3 diffs)
-
dashboard/modules/layout-templates-tab/video-manager.css (modified) (1 diff)
-
features/footer/generate-button.js (modified) (4 diffs)
-
features/footer/profile-buttons.js (modified) (3 diffs)
-
features/generate-tab/ai-provider-select.js (modified) (3 diffs)
-
features/generate-tab/video-context-mode.js (added)
-
features/layout-templates-tab/photo-positions.js (modified) (4 diffs)
-
features/layout-templates-tab/video-slot-transformation.js (added)
-
includes/class-ajax-handler.php (modified) (5 diffs)
-
includes/class-content-generator.php (modified) (21 diffs)
-
includes/class-video-manager.php (modified) (1 diff)
-
readme.txt (modified) (1 diff)
-
technodrome-ai-content-assistant.php (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
technodrome-ai-content-assistant/trunk/dashboard/modules/generate-tab/generate.php
r3423160 r3434643 247 247 <?php endif; ?> 248 248 </option> 249 <option value="ai_with_video_url_context" <?php echo $taics_user_plan === 'free' ? 'disabled' : ''; ?>> 250 🏆 <?php esc_html_e('AI + Video URL Context - Generate article from single video URL (PREMIUM)', 'technodrome-ai-content-assistant'); ?> 251 <?php if ($taics_user_plan === 'free'): ?> 252 🔒 253 <?php endif; ?> 254 </option> 255 <option value="ai_with_video_channel_context" <?php echo $taics_user_plan === 'free' ? 'disabled' : ''; ?>> 256 📡 <?php esc_html_e('AI + Video Channel Context - Generate article from entire channel (PREMIUM)', 'technodrome-ai-content-assistant'); ?> 257 <?php if ($taics_user_plan === 'free'): ?> 258 🔒 259 <?php endif; ?> 260 </option> 249 261 </select> 250 262 <div class="taics-field-help"> … … 253 265 </div> 254 266 </div> 255 <div class="taics-form-group"> 267 268 <div class="taics-form-group"> 256 269 <label for="taics-default-tone"> 257 270 📊 -
technodrome-ai-content-assistant/trunk/dashboard/modules/layout-templates-tab/layout-templates.php
r3431889 r3434643 437 437 jQuery(document).ready(function($) { 438 438 console.log('[VIDEO INLINE SCRIPT] Initializing direct video slot handlers...'); 439 440 // Check if we're in video context transformation mode 441 const isVideoContextMode = window.taicsIsVideoContextActive && window.taicsIsVideoContextActive(); 442 if (isVideoContextMode) { 443 console.log('[VIDEO INLINE SCRIPT] Video context mode active - transformation will handle slots, skipping inline handlers.'); 444 return; // Exit if transformation mode is active 445 } 439 446 440 447 // Function to toggle video input fields … … 516 523 // Direct click handler for video slot cards 517 524 $('.taics-video-slot-card').on('click', function(e) { 525 // Skip if slot is disabled/locked by transformation 526 if ($(this).hasClass('disabled') || $(this).hasClass('locked')) { 527 return; 528 } 518 529 if ($(e.target).closest('button, input').length > 0) { 519 530 return; … … 531 542 // Handle "Select Video URL" button clicks 532 543 $('.taics-btn-video').on('click', function(e) { 544 // Skip if button is disabled 545 if ($(this).prop('disabled')) { 546 return; 547 } 533 548 e.stopPropagation(); 534 549 const slotNum = $(this).data('slot'); -
technodrome-ai-content-assistant/trunk/dashboard/modules/layout-templates-tab/video-manager.css
r3401081 r3434643 404 404 background-color: #721c24 !important; 405 405 } 406 407 /* Video Context Notification - Compact above title field */ 408 .taics-video-context-notification { 409 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 410 color: white; 411 padding: 10px 15px; 412 border-radius: 6px; 413 margin-bottom: 12px; 414 display: flex; 415 align-items: center; 416 gap: 10px; 417 box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); 418 font-size: 13px; 419 } 420 421 .taics-video-context-notification .taics-notification-content { 422 display: flex; 423 align-items: center; 424 gap: 10px; 425 flex: 1; 426 } 427 428 .taics-video-context-notification .taics-notification-icon { 429 font-size: 18px; 430 } 431 432 .taics-video-context-notification .taics-notification-text { 433 flex: 1; 434 font-weight: 500; 435 } 436 437 .taics-video-context-notification .taics-notification-badge { 438 background: rgba(255, 255, 255, 0.2); 439 padding: 3px 8px; 440 border-radius: 4px; 441 font-size: 11px; 442 font-weight: 600; 443 text-transform: uppercase; 444 } 445 446 /* Dark mode for notification */ 447 body.taics-dark-mode .taics-video-context-notification, 448 [data-theme="dark"] .taics-video-context-notification { 449 background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%); 450 } 451 452 /* Video context mode styling in generate tab */ 453 .taics-topic-fixed:disabled { 454 background-color: rgba(244, 244, 245, 0.7) !important; 455 color: #64666a !important; 456 opacity: 0.8 !important; 457 } 458 459 body.taics-dark-mode .taics-topic-fixed:disabled, 460 [data-theme="dark"] .taics-topic-fixed:disabled { 461 background-color: rgba(52, 58, 64, 0.7) !important; 462 color: #adb5bd !important; 463 } 464 465 /* Under Construction Overlay for Video Slot 2 */ 466 .taics-under-construction-overlay { 467 position: absolute; 468 top: 0; 469 left: 0; 470 right: 0; 471 bottom: 0; 472 background: rgba(255, 255, 255, 0.85); 473 display: flex; 474 align-items: center; 475 justify-content: center; 476 z-index: 10; 477 border-radius: 10px; 478 } 479 480 .taics-under-construction .taics-video-preview, 481 .taics-under-construction .taics-video-actions, 482 .taics-under-construction .taics-video-url-field, 483 .taics-under-construction .taics-video-title-field { 484 opacity: 0.5; 485 pointer-events: none; 486 } 487 488 .taics-under-construction-overlay .taics-uc-content { 489 display: flex; 490 flex-direction: column; 491 align-items: center; 492 gap: 8px; 493 background: linear-gradient(135deg, #ffc107 0%, #ff9800 100%); 494 padding: 20px 30px; 495 border-radius: 12px; 496 box-shadow: 0 4px 15px rgba(255, 152, 0, 0.4); 497 } 498 499 .taics-under-construction-overlay .taics-uc-icon { 500 font-size: 32px; 501 } 502 503 .taics-under-construction-overlay .taics-uc-text { 504 font-size: 14px; 505 font-weight: 700; 506 color: #212529; 507 text-transform: uppercase; 508 letter-spacing: 2px; 509 } 510 511 /* Dark mode for under construction overlay */ 512 body.taics-dark-mode .taics-under-construction-overlay, 513 [data-theme="dark"] .taics-under-construction-overlay { 514 background: rgba(33, 37, 41, 0.9); 515 } 516 517 body.taics-dark-mode .taics-under-construction .taics-video-preview, 518 body.taics-dark-mode .taics-under-construction .taics-video-actions, 519 [data-theme="dark"] .taics-under-construction .taics-video-preview, 520 [data-theme="dark"] .taics-under-construction .taics-video-actions { 521 opacity: 0.3; 522 } 523 524 body.taics-dark-mode .taics-under-construction-overlay .taics-uc-text, 525 [data-theme="dark"] .taics-under-construction-overlay .taics-uc-text { 526 color: #fff; 527 } -
technodrome-ai-content-assistant/trunk/features/footer/generate-button.js
r3423160 r3434643 33 33 const publish = $('#taics-publish-toggle').hasClass('taics-toggle-on'); 34 34 const generateAIImage = $('#taics-ai-image-toggle').hasClass('taics-toggle-on') ? '1' : '0'; // AI Image Generation flag (v3.3.0) 35 36 if (topic.length < 3) { 35 const generationMode = $('#taics-generation-mode').val(); 36 37 // v4.0.0: Check if we're in video context mode (URL or Channel) 38 const isVideoContextMode = (generationMode === 'ai_with_video_url_context' || generationMode === 'ai_with_video_channel_context'); 39 40 // v4.0.0: Skip topic validation for video context mode 41 if (!isVideoContextMode && topic.length < 3) { 37 42 this.showNotification('Please enter a content title (minimum 3 characters).', 'error'); 38 43 $('#taics-topic').focus(); … … 73 78 this.showNotification('No active profile selected. Please select or create a profile.', 'warning'); 74 79 return; 80 } 81 82 // v4.0.0: Get video context mode from video slot transformation 83 let videoContextMode = 'video_url'; 84 const $videoSection = $('.taics-video-section'); 85 if ($videoSection.length > 0 && $videoSection.data('video_context_mode')) { 86 videoContextMode = $videoSection.data('video_context_mode'); 75 87 } 76 88 … … 80 92 publish: publish, 81 93 generate_ai_image: generateAIImage, // AI Image Generation flag (v3.3.0) 94 generation_mode: generationMode, 95 video_context_mode: videoContextMode, // v4.0.0: Video URL or Channel context 82 96 photos: photos, // Add collected photos 83 97 web_sources: webSources, // Add collected web sources … … 133 147 publish: generationData.publish, 134 148 generate_ai_image: generationData.generate_ai_image, // AI Image Generation flag (v3.3.0) 149 generation_mode: generationData.generation_mode, // v4.0.0: Include generation mode 150 video_context_mode: generationData.video_context_mode, // v4.0.0: Video URL or Channel context 135 151 photos: generationData.photos, // Pass photos to backend 136 152 web_sources: generationData.web_sources, // Pass web sources to backend -
technodrome-ai-content-assistant/trunk/features/footer/profile-buttons.js
r3421276 r3434643 137 137 this.loadCompleteProfileData(this.activeProfile); 138 138 this.isFirstProfileLoad = false; // Mark that initial load is done 139 140 // FIX: Trigger profile_loaded event for first profile load 141 // This ensures video-context-mode.js and video-slot-transformation.js initialize properly 142 setTimeout(() => { 143 $(document).trigger('taics_profile_loaded', [this.activeProfile]); 144 }, 100); 139 145 } 140 146 … … 253 259 window.TAICS_AI_Provider_Select.syncWithProfileData(profileData); 254 260 } 261 262 // FIX: Also sync Default Tone module with profile data 263 if (window.TAICS_Default_Tone && typeof window.TAICS_Default_Tone.setValue === 'function') { 264 window.TAICS_Default_Tone.setValue(profileData.default_tone || 'article-specific'); 265 } 255 266 }, 256 267 … … 336 347 }, 337 348 338 loadLayoutTemplateData: function(layoutTemplate) {349 loadLayoutTemplateData: function(layoutTemplate) { 339 350 const templateId = layoutTemplate.template_id || '1'; 340 351 -
technodrome-ai-content-assistant/trunk/features/generate-tab/ai-provider-select.js
r3423160 r3434643 288 288 updateModelOptions: function(selectedModelId = null) { 289 289 const modelSelect = $('#taics-ai-model'); 290 const currentModel = modelSelect.val(); // Get current model before clearing 291 290 292 modelSelect.empty(); 291 293 … … 298 300 }); 299 301 300 if (selectedModelId && modelSelect.find(`option[value="${selectedModelId}"]`).length) { 302 // CRITICAL FIX: When called without a specific model (e.g., from init() on tab switch), 303 // preserve the CURRENT model instead of resetting to first 304 if (selectedModelId && typeof selectedModelId === 'string' && modelSelect.find(`option[value="${selectedModelId}"]`).length) { 301 305 modelSelect.val(selectedModelId); 306 } else if (!selectedModelId && currentModel && modelSelect.find(`option[value="${currentModel}"]`).length) { 307 // Preserve current model when no specific model requested 308 modelSelect.val(currentModel); 309 } else if (selectedModelId) { 310 // Fallback to first model only if explicitly requested model not found 311 modelSelect.prop('selectedIndex', 0); 302 312 } else { 313 // No model specified and no current model - use first 303 314 modelSelect.prop('selectedIndex', 0); 304 315 } … … 671 682 672 683 $('#taics-ai-provider').val(provider); 684 $('#taics-ai-model').val(model || ''); 673 685 $('#taics-api-key').val(apiKey); 674 686 687 // CRITICAL FIX: Load models FIRST before updating options with saved model 688 // Previously, updateModelOptions(model) was called before loadModels(), 689 // causing the model list to be empty and defaulting to first model 690 this.loadModels(); 675 691 this.updateModelOptions(model); 676 692 this.updateProviderDisplay(); -
technodrome-ai-content-assistant/trunk/features/layout-templates-tab/photo-positions.js
r3421276 r3434643 16 16 selectedPhotos: {}, // Stores selected photos keyed by position: {1: {id, url, alt}, 2: {...}} 17 17 photoLinks: {}, // Stores optional photo links keyed by position: {1: "url", 2: "url", 3: "url"} 18 profilePhotosLoaded: false, // GUARD: Prevent user_meta loading from overwriting profile photos 18 19 19 20 init: function() { … … 28 29 this.bindEvents(); 29 30 30 this.loadPhotosFromUserMeta(); // LOAD na startu 31 this.loadPhotosFromUserMeta(); // LOAD na startu - only if no profile photos yet 31 32 // Listen for template changes to update the "required" status, but not to lock/unlock slots 32 33 $(document).on('taics_template_changed.photo-positions-status', this.updateStatus.bind(this)); … … 211 212 } 212 213 213 // If no selected photos, try to load from user_meta 214 // CRITICAL FIX: If profile photos were loaded, don't overwrite with user_meta 215 // Profile photos should persist - user must explicitly remove them 216 if (this.profilePhotosLoaded) { 217 const result = Object.keys(this.selectedPhotos).map(key => ({ 218 slot: parseInt(key), 219 id: this.selectedPhotos[key].id, 220 url: this.selectedPhotos[key].url, 221 alt: this.selectedPhotos[key].alt, 222 link: this.photoLinks[key] || '' 223 })).sort((a, b) => a.slot - b.slot); 224 return result; 225 } 226 227 // If no selected photos and no profile photos loaded yet, try to load from user_meta 214 228 if (Object.keys(this.selectedPhotos).length === 0) { 215 229 console.log('TAICS Photo Positions: No photos selected, loading from user_meta...'); … … 229 243 230 244 setValue: function(photosData) { 245 // Set guard flag: profile photos are being loaded 246 // This prevents getValue() from loading user_meta over profile photos 247 this.profilePhotosLoaded = true; 248 231 249 this.selectedPhotos = {}; 232 250 this.photoLinks = {}; -
technodrome-ai-content-assistant/trunk/includes/class-ajax-handler.php
r3431889 r3434643 46 46 add_action('wp_ajax_taics_clear_model_cache', [__CLASS__, 'handle_clear_model_cache']); 47 47 add_action('wp_ajax_taics_reset_license_to_free', [__CLASS__, 'handle_reset_license_to_free']); 48 49 // v4.0.0: Photo save/load handlers for photo-positions.js 50 add_action('wp_ajax_taics_save_photos', [__CLASS__, 'handle_save_photos']); 51 add_action('wp_ajax_taics_load_photos', [__CLASS__, 'handle_load_photos']); 48 52 } 49 53 … … 63 67 64 68 try { 65 if (empty($_POST['topic']) || empty($_POST['active_profile_id'])) { 69 // Get generation mode first to handle video context validation 70 $generation_mode = isset($_POST['generation_mode']) ? sanitize_text_field(wp_unslash($_POST['generation_mode'])) : 'ai_only'; 71 72 // v4.0.0: For video context mode, topic is optional (will be derived from video metadata) 73 $is_video_url_context_mode = ($generation_mode === 'ai_with_video_url_context'); 74 $is_video_channel_context_mode = ($generation_mode === 'ai_with_video_channel_context'); 75 $is_video_context_mode = $is_video_url_context_mode || $is_video_channel_context_mode; 76 77 if (!$is_video_context_mode && (empty($_POST['topic']) || empty($_POST['active_profile_id']))) { 66 78 wp_send_json_error(esc_html__('Required data missing: Topic or Profile ID is empty.', 'technodrome-ai-content-assistant')); 67 79 return; … … 75 87 'photos' => [], // Default to empty array 76 88 'web_sources' => [], // Default to empty array 77 'layout_template' => [] // Default to empty array 89 'layout_template' => [], // Default to empty array 90 'generation_mode' => isset($_POST['generation_mode']) ? sanitize_text_field(wp_unslash($_POST['generation_mode'])) : 'ai_only', // v3.9.0: Generation mode 91 'video_url' => isset($_POST['video_url']) ? esc_url_raw(wp_unslash($_POST['video_url'])) : '' // v3.9.0: Video URL for video context mode 78 92 ]; 79 93 … … 195 209 } 196 210 } 197 211 212 // v3.9.0: Video Context Mode - Get video metadata if in video context mode 213 // v4.0.0: Video URL comes from frontend POST layout_template.videos (sent as numeric array with 'slot' field) 214 if ($is_video_url_context_mode || $is_video_channel_context_mode) { 215 // Determine which slot to use based on mode 216 $selected_slot = $is_video_channel_context_mode ? 2 : 1; 217 218 // Video URL from frontend layout_template.videos 219 $video_url = ''; 220 $video_title = ''; // Custom title for channel mode 221 222 // DEBUG: Log profile_videos 223 if (defined('WP_DEBUG') && WP_DEBUG) { 224 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 225 error_log('TAICS Video Context - profile_videos: ' . print_r($profile_videos, true)); 226 error_log('TAICS Video Context - selected_slot: ' . $selected_slot); 227 } 228 229 // Use profile_videos which is already loaded - video data has 'slot' field inside each element 230 if (!empty($profile_videos) && is_array($profile_videos)) { 231 foreach ($profile_videos as $video_data) { 232 // Video data has 'slot' field to identify which slot it belongs to 233 if (is_array($video_data) && isset($video_data['slot']) && intval($video_data['slot']) === $selected_slot) { 234 if (!empty($video_data['url'])) { 235 $video_url = esc_url_raw($video_data['url']); 236 // For channel mode, also get custom title if provided 237 if ($is_video_channel_context_mode && !empty($video_data['title'])) { 238 $video_title = sanitize_text_field($video_data['title']); 239 } 240 if (defined('WP_DEBUG') && WP_DEBUG) { 241 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 242 error_log('TAICS Video Context - FOUND video URL: ' . $video_url); 243 } 244 break; 245 } 246 } 247 } 248 } 249 250 if (empty($video_url)) { 251 // No video URL found - error 252 if ($is_video_channel_context_mode) { 253 wp_send_json_error(esc_html__('No channel found in Video Slot 2. Please add a channel URL.', 'technodrome-ai-content-assistant')); 254 } else { 255 wp_send_json_error(esc_html__('No video found in Video Slot 1. Please add a video URL.', 'technodrome-ai-content-assistant')); 256 } 257 return; 258 } 259 260 // v4.0.0: For video context mode, use video/channel metadata as topic 261 $video_manager = TAICS_Video_Manager::get_instance(); 262 263 if ($is_video_channel_context_mode) { 264 // Video Channel Context mode 265 $channel_metadata = $video_manager->get_channel_metadata($video_url); 266 $args['video_channel_metadata'] = $channel_metadata; 267 $args['video_context_type'] = 'channel'; 268 269 // Get channel videos for content generation 270 $channel_videos = $video_manager->get_channel_videos($video_url, 5); 271 $args['channel_videos'] = $channel_videos; 272 273 // v4.0.4: Use video title from channel as topic, fallback to channel name or URL 274 $video_title = ''; 275 if (!empty($channel_videos) && is_array($channel_videos)) { 276 // Get title from first video in channel 277 foreach ($channel_videos as $video) { 278 if (!empty($video['title'])) { 279 $video_title = $video['title']; 280 break; 281 } 282 } 283 } 284 $args['topic'] = !empty($video_title) ? $video_title : (!empty($channel_metadata['channel_name']) ? $channel_metadata['channel_name'] : $video_url); 285 286 // Store channel name for credit in article content 287 $args['channel_name'] = $channel_metadata['channel_name'] ?? ''; 288 289 // Store channel description for content generation 290 $args['channel_description'] = $channel_metadata['channel_description'] ?? ''; 291 } else { 292 // Video URL Context mode (single video) 293 $video_context_metadata = $video_manager->get_video_context_metadata($video_url); 294 $args['video_context_metadata'] = $video_context_metadata; 295 $args['video_context_type'] = 'url'; 296 297 // v4.0.4: Use video title from metadata as topic, fallback to URL 298 $video_title = !empty($video_context_metadata['video_title']) ? $video_context_metadata['video_title'] : $video_url; 299 $args['topic'] = $video_title; 300 } 301 302 $args['video_url'] = $video_url; 303 $args['selected_video_slot'] = $selected_slot; 304 } 305 198 306 require_once plugin_dir_path(__FILE__) . 'class-content-generator.php'; 199 307 … … 1317 1425 } 1318 1426 } 1427 1428 /** 1429 * v4.0.0: Handle save photos AJAX request 1430 * Saves photos to user_meta for photo-positions.js 1431 */ 1432 public static function handle_save_photos() { 1433 check_ajax_referer('taics_ajax_nonce', 'nonce'); 1434 1435 if (!current_user_can('edit_posts')) { 1436 wp_send_json_error(esc_html__('Insufficient permissions', 'technodrome-ai-content-assistant')); 1437 return; 1438 } 1439 1440 try { 1441 if (!isset($_POST['photos']) || !is_array($_POST['photos'])) { 1442 wp_send_json_error(esc_html__('Photos data missing', 'technodrome-ai-content-assistant')); 1443 return; 1444 } 1445 1446 $user_id = get_current_user_id(); 1447 if (!$user_id) { 1448 wp_send_json_error(esc_html__('User not logged in', 'technodrome-ai-content-assistant')); 1449 return; 1450 } 1451 1452 // Sanitize photos data 1453 $photos_raw = map_deep(wp_unslash($_POST['photos']), 'sanitize_text_field'); 1454 $photos = []; 1455 1456 foreach ($photos_raw as $photo_data) { 1457 if (is_array($photo_data)) { 1458 $photos[] = [ 1459 'slot' => isset($photo_data['slot']) ? intval($photo_data['slot']) : 0, 1460 'id' => isset($photo_data['id']) ? intval($photo_data['id']) : 0, 1461 'url' => isset($photo_data['url']) ? esc_url_raw($photo_data['url']) : '', 1462 'alt' => isset($photo_data['alt']) ? sanitize_text_field($photo_data['alt']) : '', 1463 'link' => isset($photo_data['link']) ? esc_url_raw($photo_data['link']) : '' 1464 ]; 1465 } 1466 } 1467 1468 $result = update_user_meta($user_id, 'taics_photos', $photos); 1469 1470 if ($result !== false) { 1471 wp_send_json_success([ 1472 'message' => esc_html__('Photos saved successfully', 'technodrome-ai-content-assistant'), 1473 'photos' => $photos 1474 ]); 1475 } else { 1476 wp_send_json_error(esc_html__('Failed to save photos', 'technodrome-ai-content-assistant')); 1477 } 1478 } catch (Exception $e) { 1479 wp_send_json_error(esc_html__('Failed to save photos', 'technodrome-ai-content-assistant')); 1480 } 1481 } 1482 1483 /** 1484 * v4.0.0: Handle load photos AJAX request 1485 * Loads photos from user_meta for photo-positions.js 1486 */ 1487 public static function handle_load_photos() { 1488 check_ajax_referer('taics_ajax_nonce', 'nonce'); 1489 1490 if (!current_user_can('edit_posts')) { 1491 wp_send_json_error(esc_html__('Insufficient permissions', 'technodrome-ai-content-assistant')); 1492 return; 1493 } 1494 1495 try { 1496 $user_id = get_current_user_id(); 1497 if (!$user_id) { 1498 wp_send_json_error(esc_html__('User not logged in', 'technodrome-ai-content-assistant')); 1499 return; 1500 } 1501 1502 $photos = get_user_meta($user_id, 'taics_photos', true); 1503 1504 if (!$photos || !is_array($photos)) { 1505 $photos = []; 1506 } 1507 1508 wp_send_json_success([ 1509 'photos' => $photos 1510 ]); 1511 } catch (Exception $e) { 1512 wp_send_json_error(esc_html__('Failed to load photos', 'technodrome-ai-content-assistant')); 1513 } 1514 } 1319 1515 } -
technodrome-ai-content-assistant/trunk/includes/class-content-generator.php
r3431889 r3434643 32 32 */ 33 33 public static function generate($args) { 34 if (empty($args['topic'])) { 34 // v4.0.0: For video context mode, topic is derived from video metadata 35 // Check if we have video context metadata which will provide the topic 36 $generation_mode = $args['generation_mode'] ?? 'ai_only'; 37 $is_video_context_mode = ($generation_mode === 'ai_with_video_context' || $generation_mode === 'ai_with_video_url_context' || $generation_mode === 'ai_with_video_channel_context'); 38 39 if (!$is_video_context_mode && empty($args['topic'])) { 35 40 throw new Exception(esc_html__('Topic is required', 'technodrome-ai-content-assistant')); 41 } 42 43 // For video context mode, topic should be set from video metadata in AJAX handler 44 // But if somehow it's still empty, try to use video context title 45 if ($is_video_context_mode && empty($args['topic'])) { 46 if (!empty($args['video_context_metadata']['title'])) { 47 $args['topic'] = $args['video_context_metadata']['title']; 48 } elseif (!empty($args['video_context_metadata']['video_url'])) { 49 $args['topic'] = $args['video_context_metadata']['video_url']; 50 } else { 51 throw new Exception(esc_html__('Video context metadata is required for video context mode', 'technodrome-ai-content-assistant')); 52 } 36 53 } 37 54 … … 53 70 $generation_args['topic'] = sanitize_text_field($args['topic']); 54 71 $generation_args['publish'] = !empty($args['publish']); // Sent from frontend 72 $generation_args['generation_mode'] = $args['generation_mode'] ?? $generation_args['generation_mode'] ?? 'ai_only'; 73 74 // v4.0.3: Pass video context data to generation 75 if (!empty($args['video_url'])) { 76 $generation_args['video_url'] = $args['video_url']; 77 } 78 if (!empty($args['video_context_metadata'])) { 79 $generation_args['video_context_metadata'] = $args['video_context_metadata']; 80 } 81 if (!empty($args['video_channel_metadata'])) { 82 $generation_args['video_channel_metadata'] = $args['video_channel_metadata']; 83 } 55 84 56 85 // CRITICAL FIX v3.4.2: Merge photos, videos, layout_template, and web_sources from AJAX args … … 451 480 // Correctly map data from the loaded profile structure ($args) 452 481 $ai_args = array( 453 'topic' => $args['topic'] ?? '', 454 'language' => $args['language'] ?? 'en-US', 455 'model' => $args['ai_settings']['ai_model'] ?? '', 456 'api_key' => $api_key, 457 'tone' => $args['default_tone'] ?? 'professional', 458 'content_length' => $args['content_length'] ?? 'medium', 459 'content_type' => $args['content_type'] ?? 'blog', 460 'category' => $args['category'] ?? 1, 461 'word_count' => self::get_word_count_from_length($args['content_length'] ?? 'medium'), 462 'generation_mode' => $args['generation_mode'] ?? 'ai_only', 463 'headings' => $args['content_rules']['headings'] ?? '', 464 'guidelines' => $args['content_rules']['guidelines'] ?? '', 465 'web_sources' => $args['content_rules']['web_sources'] ?? array() 482 'topic' => $args['topic'] ?? '', 483 'language' => $args['language'] ?? 'en-US', 484 'model' => $args['ai_settings']['ai_model'] ?? '', 485 'api_key' => $api_key, 486 'tone' => $args['default_tone'] ?? 'professional', 487 'content_length' => $args['content_length'] ?? 'medium', 488 'content_type' => $args['content_type'] ?? 'blog', 489 'category' => $args['category'] ?? 1, 490 'word_count' => self::get_word_count_from_length($args['content_length'] ?? 'medium'), 491 'generation_mode' => $args['generation_mode'] ?? 'ai_only', 492 'headings' => $args['content_rules']['headings'] ?? '', 493 'guidelines' => $args['content_rules']['guidelines'] ?? '', 494 'web_sources' => $args['content_rules']['web_sources'] ?? array(), 495 'video_context_metadata' => $args['video_context_metadata'] ?? array() // v3.9.0: Video Context 466 496 ); 467 497 … … 552 582 } 553 583 554 $title = ucfirst(trim($args['topic'])); 584 // v4.0.0: Video Context Attribution - Support both old and new mode names 585 // v4.0.1: REMOVED duplicate video embed - video is now added ONLY in insert_videos_into_content() 586 $generation_mode = $args['generation_mode'] ?? 'ai_only'; 587 $is_video_context = ($generation_mode === 'ai_with_video_context' || $generation_mode === 'ai_with_video_url_context' || $generation_mode === 'ai_with_video_channel_context'); 588 589 if ($is_video_context && !empty($args['video_url'])) { 590 // Add attribution section (without duplicate video embed) 591 $video_platform = !empty($args['video_context_metadata']['video_platform']) ? $args['video_context_metadata']['video_platform'] : 'Video'; 592 $video_author = !empty($args['video_context_metadata']['video_author']) ? $args['video_context_metadata']['video_author'] : ''; 593 $channel_name = !empty($args['video_channel_metadata']['channel_name']) ? $args['video_channel_metadata']['channel_name'] : ''; 594 595 $content .= "\n\n<div class='video-source-attribution' style='background: #f0f0f1; padding: 15px; margin: 20px 0; border-left: 4px solid #2271b1; border-radius: 4px;'>"; 596 $content .= "<p style='margin: 0 0 10px 0;'><strong>📺 Source:</strong> " . esc_html($video_platform) . "</p>"; 597 if (!empty($channel_name)) { 598 $content .= "<p style='margin: 0 0 10px 0;'><strong>👤 Channel:</strong> " . esc_html($channel_name) . "</p>"; 599 } elseif (!empty($video_author)) { 600 $content .= "<p style='margin: 0 0 10px 0;'><strong>👤 Creator:</strong> " . esc_html($video_author) . "</p>"; 601 } 602 $content .= "<p style='margin: 0 0 10px 0;'><strong>🎬 Original:</strong> <a href='" . esc_url($args['video_url']) . "' target='_blank' rel='noopener noreferrer'>Watch Video</a></p>"; 603 $content .= "</div>"; 604 } elseif ($generation_mode === 'ai_with_video_context' && !empty($args['video_context_metadata'])) { 605 // Legacy support 606 $video_context = $args['video_context_metadata']; 607 $content .= "\n\n<div class='video-source-attribution' style='background: #f0f0f1; padding: 15px; margin: 20px 0; border-left: 4px solid #2271b1; border-radius: 4px;'>"; 608 $content .= "<p style='margin: 0 0 10px 0;'><strong>📺 Video Source:</strong> " . esc_html($video_context['video_platform']) . "</p>"; 609 $content .= "<p style='margin: 0 0 10px 0;'><strong>👤 Creator:</strong> " . esc_html($video_context['video_author']) . "</p>"; 610 $content .= "<p style='margin: 0 0 10px 0;'><strong>🎬 Original Video:</strong> <a href='" . esc_url($video_context['video_url']) . "' target='_blank' rel='noopener noreferrer'>Watch</a></p>"; 611 if (!empty($video_context['video_title'])) { 612 $content .= "<p style='margin: 0;'><strong>📝 Based on:</strong> " . esc_html($video_context['video_title']) . "</p>"; 613 } 614 $content .= "</div>"; 615 } 616 617 // v4.0.1: Title translation - for video context modes, generate title in selected language 618 $title = self::generate_translated_title($args); 555 619 $post_id = self::create_post($title, $content, $args); 556 620 … … 607 671 $prompt = ''; 608 672 609 // Za 'ai_only' i 'ai_with_rules' kreiramo standardni uvod 610 if ($generation_mode === 'ai_only' || $generation_mode === 'ai_with_rules') { 673 // v3.9.0: AI + Video Context Mode (NEW) 674 // v4.0.1: Use TOPIC as subject, video title is just context information 675 if ($generation_mode === 'ai_with_video_context' || $generation_mode === 'ai_with_video_url_context' || $generation_mode === 'ai_with_video_channel_context') { 676 $prompt .= "You are an expert writer creating an article for a website. Your task is to write a high-quality, well-structured {$content_type}.\n\n"; 677 $prompt .= "VIDEO CONTEXT: 678 "; 679 680 if (!empty($args['video_context_metadata'])) { 681 $video_context = $args['video_context_metadata']; 682 $prompt .= "Platform: {$video_context['video_platform']}\n"; 683 $prompt .= "Creator: {$video_context['video_author']}\n"; 684 $prompt .= "Video Title: {$video_context['video_title']}\n"; 685 $prompt .= "Video URL: {$video_context['video_url']}\n\n"; 686 } 687 688 // v4.0.1: Use topic as the main subject for article generation 689 $prompt .= "ARTICLE TOPIC: {$topic}\n"; 690 $prompt .= "IMPORTANT: The article, including the title, MUST be written in {$language_name}.\n"; 691 $prompt .= "LANGUAGE: {$language_name}\n"; 692 $prompt .= "TONE OF VOICE: {$tone}\n"; 693 $prompt .= "APPROXIMATE WORD COUNT: {$target_words} words\n\n"; 694 } elseif ($generation_mode === 'ai_only' || $generation_mode === 'ai_with_rules') { 695 // Za 'ai_only' i 'ai_with_rules' kreiramo standardni uvod 611 696 $prompt .= "You are an expert writer creating an article for a website. Your task is to write a high-quality, well-structured {$content_type}.\n\n"; 612 697 $prompt .= "TOPIC: {$topic}\n"; … … 842 927 843 928 /** 844 * Helper: Insert HTML after a node 929 * Helper: Insert HTML after a node - FIXED for complex HTML like iframes 845 930 */ 846 931 private static function insert_after_node($dom, $node, $html) { 847 932 if (!$node) return; 848 933 934 // Remove newlines and excessive whitespace from HTML for XML compatibility 935 $html = preg_replace('/\s+/', ' ', $html); 936 $html = trim($html); 937 938 // Try document fragment first (works for simple HTML) 849 939 $fragment = $dom->createDocumentFragment(); 850 851 // v3.5.0 FIXED: Validate fragment before insertion852 940 $xml_loaded = $fragment->appendXML($html); 853 941 854 // Don't insert empty fragments855 if ($xml_loaded === false || $fragment->childNodes->length === 0) {856 if (defined('WP_DEBUG') && WP_DEBUG) {857 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log858 error_log('TAICS: Failed to create document fragment from HTML: ' . substr($html, 0, 100));942 if ($xml_loaded !== false && $fragment->childNodes->length > 0) { 943 if ($node->nextSibling) { 944 $node->parentNode->insertBefore($fragment, $node->nextSibling); 945 } else { 946 $node->parentNode->appendChild($fragment); 859 947 } 860 948 return; 861 949 } 862 950 863 if ($node->nextSibling) { 864 $node->parentNode->insertBefore($fragment, $node->nextSibling); 865 } else { 866 $node->parentNode->appendChild($fragment); 867 } 868 } 869 870 /** 871 * Helper: Insert HTML before a node 951 // Fallback: Use loadHTML approach for complex HTML (iframes, etc.) 952 $temp_doc = new DOMDocument('1.0', 'UTF-8'); 953 libxml_use_internal_errors(true); 954 $temp_doc->loadHTML('<?xml encoding="UTF-8"><html><body>' . $html . '</body></html>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); 955 libxml_clear_errors(); 956 957 $imported_node = $dom->importNode($temp_doc->documentElement->firstChild, true); 958 959 if ($imported_node) { 960 if ($node->nextSibling) { 961 $node->parentNode->insertBefore($imported_node, $node->nextSibling); 962 } else { 963 $node->parentNode->appendChild($imported_node); 964 } 965 } 966 } 967 968 /** 969 * Helper: Insert HTML before a node - FIXED for complex HTML like iframes 872 970 */ 873 971 private static function insert_before_node($dom, $node, $html) { 874 972 if (!$node) return; 875 973 974 // Remove newlines and excessive whitespace from HTML for XML compatibility 975 $html = preg_replace('/\s+/', ' ', $html); 976 $html = trim($html); 977 978 // Try document fragment first (works for simple HTML) 876 979 $fragment = $dom->createDocumentFragment(); 877 878 // v3.5.0 FIXED: Validate fragment before insertion879 980 $xml_loaded = $fragment->appendXML($html); 880 981 881 // Don't insert empty fragments 882 if ($xml_loaded === false || $fragment->childNodes->length === 0) { 883 if (defined('WP_DEBUG') && WP_DEBUG) { 884 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 885 error_log('TAICS: Failed to create document fragment from HTML: ' . substr($html, 0, 100)); 886 } 982 if ($xml_loaded !== false && $fragment->childNodes->length > 0) { 983 $node->parentNode->insertBefore($fragment, $node); 887 984 return; 888 985 } 889 986 890 $node->parentNode->insertBefore($fragment, $node); 987 // Fallback: Use loadHTML approach for complex HTML (iframes, etc.) 988 $temp_doc = new DOMDocument('1.0', 'UTF-8'); 989 libxml_use_internal_errors(true); 990 $temp_doc->loadHTML('<?xml encoding="UTF-8"><html><body>' . $html . '</body></html>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); 991 libxml_clear_errors(); 992 993 $imported_node = $dom->importNode($temp_doc->documentElement->firstChild, true); 994 995 if ($imported_node) { 996 $node->parentNode->insertBefore($imported_node, $node); 997 } 891 998 } 892 999 … … 920 1027 private static function process_advanced_template_elements($canvas_data, $image_blocks, $ai_content, $videos = array()) { 921 1028 if (empty($canvas_data) || !is_array($canvas_data)) { 1029 // v4.0.4 FIX: Return original AI content if no canvas data 922 1030 return $ai_content; 923 1031 } … … 1081 1189 1082 1190 return $post_id; 1191 } 1192 1193 /** 1194 * v4.0.1: Generate title - use topic as-is (AI handles translation in prompt) 1195 * For all modes, use the topic which AI has already processed in the selected language 1196 */ 1197 private static function generate_translated_title($args) { 1198 // v4.0.1: Simply use the topic - AI prompt already includes language instruction 1199 // The topic should already be in the correct language based on the AI prompt 1200 $topic = $args['topic'] ?? ''; 1201 if (!empty($topic)) { 1202 $title = ucfirst(trim($topic)); 1203 // Clean up special characters 1204 $title = preg_replace('/[^\p{L}\p{N}\s\-_]/u', '', $title); 1205 return $title; 1206 } 1207 return ''; 1083 1208 } 1084 1209 … … 1288 1413 /** 1289 1414 * Insert videos into generated content 1415 * Fixed to avoid duplicate videos in Template 6 1290 1416 */ 1291 1417 private static function insert_videos_into_content($content, $args) { 1292 // CRITICAL FIX: Use layout_template from AJAX request, not from profile 1418 // Check for video context mode FIRST 1419 $generation_mode = $args['generation_mode'] ?? 'ai_only'; 1420 $is_video_context = ($generation_mode === 'ai_with_video_context' || 1421 $generation_mode === 'ai_with_video_url_context' || 1422 $generation_mode === 'ai_with_video_channel_context'); 1423 1424 $video_context_url = isset($args['video_url']) ? $args['video_url'] : ''; 1425 1426 // For video context mode, ONLY use video_url from args, ignore layout_template videos 1427 if ($is_video_context && !empty($video_context_url)) { 1428 $video_manager = TAICS_Video_Manager::get_instance(); 1429 $video_html = $video_manager->get_video_html($video_context_url, ''); 1430 if (!empty($video_html)) { 1431 $content = '<div class="taics-video-container">' . $video_html . '</div>' . "\n\n" . $content; 1432 } 1433 return $content; 1434 } 1435 1436 // Get template info 1293 1437 $layout_template = isset($args['layout_template']) ? $args['layout_template'] : []; 1294 1438 $template_id = isset($layout_template['template_id']) ? intval($layout_template['template_id']) : 1; 1439 $advanced_canvas = isset($layout_template['advanced_template_canvas']) ? $layout_template['advanced_template_canvas'] : []; 1440 1441 // For Template 6 WITH canvas data, videos are handled in process_advanced_template_elements 1442 // Skip video insertion to avoid duplicates 1443 if ($template_id == 6 && !empty($advanced_canvas) && is_array($advanced_canvas)) { 1444 return $content; 1445 } 1446 1447 // Get videos array 1295 1448 $videos = isset($args['videos']) && is_array($args['videos']) ? $args['videos'] : []; 1296 1449 1297 // Template 1 or no videos 1450 // Template 1 or no videos - return early 1298 1451 if ($template_id == 1 || empty($videos)) { 1299 1452 return $content; … … 1304 1457 $video_manager = TAICS_Video_Manager::get_instance(); 1305 1458 1306 // AUTOSAVE REVOLUCIJA:Support both old format (associative array) and new profile format (array of objects)1459 // Support both old format (associative array) and new profile format (array of objects) 1307 1460 foreach ($videos as $slot => $video_data) { 1308 1461 $video_url = ''; 1309 1462 $video_title = ''; 1310 $actual_slot = $slot; // Default to array index1463 $actual_slot = $slot; 1311 1464 1312 1465 // New profile format: array of objects with {slot, platform, video_id, url, thumbnail} … … 1315 1468 $video_url = $video_array['url'] ?? ''; 1316 1469 $video_title = $video_array['title'] ?? ''; 1317 // v3.4.2: Use slot from video object if available1318 1470 if (isset($video_array['slot'])) { 1319 1471 $actual_slot = intval($video_array['slot']); … … 1330 1482 1331 1483 if (!empty($video_html)) { 1332 // Wrap video in container with proper styling1333 1484 $video_blocks[$actual_slot] = sprintf( 1334 1485 '<div class="taics-video-container">%s</div>', … … 1344 1495 } 1345 1496 1346 // Sort videos by slot number 1497 // Sort videos by slot number and re-index 1347 1498 ksort($video_blocks); 1348 1499 $video_blocks = array_values($video_blocks); 1349 1500 1350 // Use DOM manipulation for precise insertion like images 1501 // Ensure content is not empty before processing 1502 if (empty(trim($content))) { 1503 return $content; 1504 } 1505 1506 // Use DOM manipulation for precise insertion 1351 1507 $dom = new DOMDocument('1.0', 'UTF-8'); 1352 1508 libxml_use_internal_errors(true); … … 1355 1511 1356 1512 $xpath = new DOMXPath($dom); 1357 1358 // Find all block-level elements (p, h1-h6, ul, ol, blockquote, etc.)1359 1513 $blocks = $xpath->query('//body/*'); 1360 1514 $total_blocks = $blocks->length; 1361 1515 1362 1516 if ($total_blocks == 0) { 1363 // Fallback: simple string insertion1364 1517 return self::insert_videos_simple($content, $video_blocks, $template_id); 1365 1518 } 1366 1519 1367 1520 switch ($template_id) { 1368 case 2: // Single Video - after first block1521 case 2: 1369 1522 if (isset($video_blocks[0]) && $blocks->length > 0) { 1370 1523 $first_block = $blocks->item(0); … … 1373 1526 break; 1374 1527 1375 case 3: // Two Videos - after first, then middle1528 case 3: 1376 1529 if (isset($video_blocks[0]) && $blocks->length > 0) { 1377 1530 $first_block = $blocks->item(0); 1378 1531 self::insert_after_node($dom, $first_block, $video_blocks[0]); 1379 1532 } 1380 1381 // Re-query after first insertion1382 1533 $blocks = $xpath->query('//body/*'); 1383 1534 if (isset($video_blocks[1]) && $blocks->length > 3) { … … 1387 1538 break; 1388 1539 1389 case 4: // Three Videos - first, third, middle1540 case 4: 1390 1541 if (isset($video_blocks[0]) && $blocks->length > 0) { 1391 1542 $first_block = $blocks->item(0); 1392 1543 self::insert_after_node($dom, $first_block, $video_blocks[0]); 1393 1544 } 1394 1395 // Re-query after first insertion1396 1545 $blocks = $xpath->query('//body/*'); 1397 1546 if (isset($video_blocks[1]) && $blocks->length > 2) { … … 1399 1548 self::insert_after_node($dom, $third_block, $video_blocks[1]); 1400 1549 } 1401 1402 // Re-query after second insertion1403 1550 $blocks = $xpath->query('//body/*'); 1404 1551 if (isset($video_blocks[2]) && $blocks->length > 4) { … … 1408 1555 break; 1409 1556 1410 case 5: // Magazine style - distributed1557 case 5: 1411 1558 if (isset($video_blocks[0]) && $blocks->length > 0) { 1412 1559 $first_block = $blocks->item(0); 1413 1560 self::insert_after_node($dom, $first_block, $video_blocks[0]); 1414 1561 } 1415 1416 1562 $blocks = $xpath->query('//body/*'); 1417 1563 $total = $blocks->length; 1418 1419 1564 if (isset($video_blocks[1]) && $total > 4) { 1420 1565 $pos = floor($total / 3); … … 1422 1567 self::insert_after_node($dom, $target, $video_blocks[1]); 1423 1568 } 1424 1425 1569 $blocks = $xpath->query('//body/*'); 1426 1570 $total = $blocks->length; 1427 1428 1571 if (isset($video_blocks[2]) && $total > 6) { 1429 1572 $pos = floor(($total * 2) / 3); … … 1433 1576 break; 1434 1577 1435 case 6: // Custom template - use specific video slots1436 // Insert at specific positions based on template logic1578 case 6: 1579 // Template 6 Custom Builder fallback grid - insert at specific positions 1437 1580 if (isset($video_blocks[0]) && $blocks->length > 1) { 1438 // Video 1 after introduction paragraph1439 1581 $intro_block = $blocks->item(0); 1440 1582 self::insert_after_node($dom, $intro_block, $video_blocks[0]); 1441 1583 } 1442 1443 1584 $blocks = $xpath->query('//body/*'); 1444 1585 if (isset($video_blocks[1]) && $blocks->length > 4) { 1445 // Video 2 after main content1446 1586 $main_content_block = $blocks->item(3); 1447 1587 self::insert_after_node($dom, $main_content_block, $video_blocks[1]); … … 1452 1592 // Save modified content 1453 1593 $body = $dom->getElementsByTagName('body')->item(0); 1454 $html = $dom->saveHTML($body); 1455 return $html; 1594 if ($body) { 1595 $output = ''; 1596 foreach ($body->childNodes as $node) { 1597 $html = $dom->saveHTML($node); 1598 $output .= $html; 1599 } 1600 return $output; 1601 } 1602 1603 return $content; 1456 1604 } 1457 1605 -
technodrome-ai-content-assistant/trunk/includes/class-video-manager.php
r3401081 r3434643 352 352 } 353 353 } 354 355 /** 356 * Get oEmbed metadata from video URL 357 * 358 * @param string $url Video URL 359 * @return array|false 360 */ 361 public function get_oembed_metadata($url) { 362 if (empty($url)) { 363 return false; 364 } 365 366 $url = esc_url($url); 367 $oembed_url = ''; 368 369 // Determine oEmbed endpoint based on platform 370 if (strpos($url, 'youtube.com') !== false || strpos($url, 'youtu.be') !== false) { 371 $oembed_url = 'https://www.youtube.com/oembed?url=' . urlencode($url) . '&format=json'; 372 } elseif (strpos($url, 'vimeo.com') !== false) { 373 $oembed_url = 'https://vimeo.com/api/oembed.json?url=' . urlencode($url); 374 } elseif (strpos($url, 'tiktok.com') !== false) { 375 $oembed_url = 'https://www.tiktok.com/oembed?url=' . urlencode($url); 376 } else { 377 // Fallback to noembed.com for other platforms 378 $oembed_url = 'https://noembed.com/embed?url=' . urlencode($url); 379 } 380 381 // Fetch oEmbed data 382 $response = wp_remote_get($oembed_url, array('timeout' => 10)); 383 384 if (is_wp_error($response)) { 385 return false; 386 } 387 388 $body = wp_remote_retrieve_body($response); 389 $data = json_decode($body, true); 390 391 if (empty($data)) { 392 return false; 393 } 394 395 // Extract relevant metadata 396 $metadata = array( 397 'title' => $data['title'] ?? '', 398 'author_name' => $data['author_name'] ?? '', 399 'author_url' => $data['author_url'] ?? '', 400 'thumbnail_url' => $data['thumbnail_url'] ?? '', 401 'type' => $data['type'] ?? 'video', 402 'html' => $data['html'] ?? '' 403 ); 404 405 return $metadata; 406 } 407 408 /** 409 * Get complete video context metadata 410 * Combines platform metadata with oEmbed data 411 * 412 * @param string $url Video URL 413 * @return array 414 */ 415 public function get_video_context_metadata($url) { 416 if (empty($url)) { 417 return array(); 418 } 419 420 // Get platform metadata 421 $platform_metadata = $this->get_video_metadata($url); 422 423 if (empty($platform_metadata)) { 424 return array(); 425 } 426 427 // Get oEmbed metadata 428 $oembed_metadata = $this->get_oembed_metadata($url); 429 430 // Combine all metadata 431 $context_metadata = array( 432 'video_url' => $url, 433 'video_platform' => $platform_metadata['platform'] ?? 'unknown', 434 'video_id' => $platform_metadata['video_id'] ?? '', 435 'video_title' => $oembed_metadata['title'] ?? $url, // Fallback to URL if no title 436 'video_author' => $oembed_metadata['author_name'] ?? 'Unknown', 437 'video_author_url' => $oembed_metadata['author_url'] ?? '', 438 'thumbnail_url' => $oembed_metadata['thumbnail_url'] ?? '', 439 'oembed_html' => $oembed_metadata['html'] ?? '' 440 ); 441 442 return $context_metadata; 443 } 444 445 /** 446 * Check if URL is a video channel URL (not a single video) 447 * 448 * @param string $url URL to check 449 * @return bool 450 */ 451 public function is_channel_url($url) { 452 if (empty($url)) { 453 return false; 454 } 455 456 // YouTube channel patterns 457 $youtube_channel_patterns = array( 458 '/youtube\.com\/@([a-zA-Z0-9_-]+)/', // Custom URL @username 459 '/youtube\.com\/channel\/([A-Za-z0-9_-]+)/', // Legacy channel ID 460 '/youtube\.com\/c\/([a-zA-Z0-9_-]+)/', // Custom URL /c/ 461 '/youtube\.com\/user\/([a-zA-Z0-9_-]+)/', // Legacy username 462 '/youtube\.com\/playlist\?list=([a-zA-Z0-9_-]+)/' // Playlist (can represent channel uploads) 463 ); 464 465 // Vimeo channel/user patterns 466 $vimeo_channel_patterns = array( 467 '/vimeo\.com\/([a-zA-Z0-9_-]+)/', // User or channel page 468 '/vimeo\.com\/channels\/([a-zA-Z0-9_-]+)/' // Channel page 469 ); 470 471 // TikTok profile patterns 472 $tiktok_patterns = array( 473 '/tiktok\.com\/@([a-zA-Z0-9_.]+)[\/]?$/', // Profile page 474 ); 475 476 // Check YouTube 477 foreach ($youtube_channel_patterns as $pattern) { 478 if (preg_match($pattern, $url, $match)) { 479 // Exclude single video URLs (they have /watch?v=) 480 if (strpos($url, '/watch?v=') === false && strpos($url, 'youtu.be/') === false) { 481 return true; 482 } 483 } 484 } 485 486 // Check Vimeo - exclude single video URLs 487 if (preg_match('/vimeo\.com\/(\d+)/', $url, $match)) { 488 // If it's a numeric ID, check if it's likely a video (single video) or user page 489 // Vimeo video URLs typically have just numbers, while user pages might have paths 490 if (preg_match('/vimeo\.com\/(\d+)\/?$/', $url)) { 491 // Single number at end - could be video or user, check if there's a video ID 492 // This is a fallback - we'll try to fetch metadata to determine 493 return true; // Assume channel/user for now 494 } 495 } 496 497 // Check TikTok profile 498 foreach ($tiktok_patterns as $pattern) { 499 if (preg_match($pattern, $url, $match)) { 500 // Exclude video URLs 501 if (strpos($url, '/video/') === false) { 502 return true; 503 } 504 } 505 } 506 507 return false; 508 } 509 510 /** 511 * Get channel metadata from channel URL 512 * 513 * @param string $url Channel URL 514 * @return array 515 */ 516 public function get_channel_metadata($url) { 517 if (empty($url)) { 518 return array(); 519 } 520 521 $channel_metadata = array( 522 'channel_url' => $url, 523 'channel_type' => 'unknown', 524 'channel_id' => '', 525 'channel_name' => '', 526 'channel_description' => '', 527 'subscriber_count' => '', 528 'video_count' => '', 529 'thumbnail_url' => '', 530 'profile_url' => '' 531 ); 532 533 // Determine platform and extract channel info 534 if (strpos($url, 'youtube.com') !== false || strpos($url, 'youtu.be') !== false) { 535 $channel_metadata['channel_type'] = 'youtube'; 536 537 // Extract channel identifier 538 if (preg_match('/youtube\.com\/@([a-zA-Z0-9_-]+)/', $url, $match)) { 539 $channel_metadata['channel_id'] = $match[1]; 540 $channel_metadata['channel_name'] = '@' . $match[1]; 541 } elseif (preg_match('/youtube\.com\/channel\/([A-Za-z0-9_-]+)/', $url, $match)) { 542 $channel_metadata['channel_id'] = $match[1]; 543 } elseif (preg_match('/youtube\.com\/c\/([a-zA-Z0-9_-]+)/', $url, $match)) { 544 $channel_metadata['channel_id'] = $match[1]; 545 $channel_metadata['channel_name'] = $match[1]; 546 } elseif (preg_match('/youtube\.com\/user\/([a-zA-Z0-9_-]+)/', $url, $match)) { 547 $channel_metadata['channel_id'] = $match[1]; 548 $channel_metadata['channel_name'] = $match[1]; 549 } elseif (preg_match('/youtube\.com\/playlist\?list=([a-zA-Z0-9_-]+)/', $url, $match)) { 550 $channel_metadata['channel_id'] = $match[1]; 551 $channel_metadata['channel_type'] = 'youtube_playlist'; 552 $channel_metadata['channel_name'] = 'Playlist'; 553 } 554 555 // Try to get channel info from YouTube API or oEmbed 556 $channel_metadata = $this-> enrich_youtube_channel_metadata($channel_metadata); 557 558 } elseif (strpos($url, 'vimeo.com') !== false) { 559 $channel_metadata['channel_type'] = 'vimeo'; 560 561 if (preg_match('/vimeo\.com\/channels\/([a-zA-Z0-9_-]+)/', $url, $match)) { 562 $channel_metadata['channel_id'] = $match[1]; 563 $channel_metadata['channel_name'] = 'Channel: ' . $match[1]; 564 } elseif (preg_match('/vimeo\.com\/([a-zA-Z0-9_-]+)/', $url, $match) && strpos($url, '/channels/') === false) { 565 $channel_metadata['channel_id'] = $match[1]; 566 $channel_metadata['channel_name'] = $match[1]; 567 } 568 569 // Try to get channel info from Vimeo 570 $channel_metadata = $this->enrich_vimeo_channel_metadata($channel_metadata); 571 572 } elseif (strpos($url, 'tiktok.com') !== false) { 573 $channel_metadata['channel_type'] = 'tiktok'; 574 575 if (preg_match('/tiktok\.com\/@([a-zA-Z0-9_.]+)/', $url, $match)) { 576 $channel_metadata['channel_id'] = $match[1]; 577 $channel_metadata['channel_name'] = '@' . $match[1]; 578 } 579 } 580 581 return $channel_metadata; 582 } 583 584 /** 585 * Enrich YouTube channel metadata with additional info 586 * 587 * @param array $channel_metadata Channel metadata 588 * @return array 589 */ 590 private function enrich_youtube_channel_metadata($channel_metadata) { 591 // Try to get oEmbed data for the channel 592 // Note: YouTube oEmbed doesn't directly support channels, but we can try 593 594 // For now, return with available info 595 // In production, you'd use YouTube Data API to get subscriber count, etc. 596 597 if (!empty($channel_metadata['channel_id'])) { 598 $channel_metadata['profile_url'] = 'https://www.youtube.com/channel/' . $channel_metadata['channel_id']; 599 } 600 601 return $channel_metadata; 602 } 603 604 /** 605 * Enrich Vimeo channel metadata with additional info 606 * 607 * @param array $channel_metadata Channel metadata 608 * @return array 609 */ 610 private function enrich_vimeo_channel_metadata($channel_metadata) { 611 // Try to get oEmbed data for the channel 612 if (!empty($channel_metadata['channel_id'])) { 613 // Try to fetch channel info from Vimeo 614 $channel_url = 'https://vimeo.com/' . $channel_metadata['channel_id']; 615 $response = wp_remote_get($channel_url, array('timeout' => 10)); 616 617 if (!is_wp_error($response)) { 618 $body = wp_remote_retrieve_body($response); 619 620 // Look for creator name in page 621 if (preg_match('/<span class="\s*clip_info-title[^>]*>([^<]+)<\/span>/', $body, $match)) { 622 $channel_metadata['channel_name'] = trim($match[1]); 623 } 624 } 625 } 626 627 return $channel_metadata; 628 } 629 630 /** 631 * Get videos from a channel (for Video Channel Context mode) 632 * 633 * @param string $channel_url Channel URL 634 * @param int $max_videos Maximum number of videos to retrieve 635 * @return array 636 */ 637 public function get_channel_videos($channel_url, $max_videos = 5) { 638 if (empty($channel_url)) { 639 return array(); 640 } 641 642 $videos = array(); 643 644 // Determine platform and get videos accordingly 645 if (strpos($channel_url, 'youtube.com') !== false) { 646 $videos = $this->get_youtube_channel_videos($channel_url, $max_videos); 647 } elseif (strpos($channel_url, 'vimeo.com') !== false) { 648 $videos = $this->get_vimeo_channel_videos($channel_url, $max_videos); 649 } elseif (strpos($channel_url, 'tiktok.com') !== false) { 650 $videos = $this->get_tiktok_profile_videos($channel_url, $max_videos); 651 } 652 653 return $videos; 654 } 655 656 /** 657 * Get videos from YouTube channel 658 * 659 * @param string $channel_url Channel URL 660 * @param int $max_videos Maximum videos 661 * @return array 662 */ 663 private function get_youtube_channel_videos($channel_url, $max_videos) { 664 $videos = array(); 665 666 // For now, try to get recent video from channel page 667 // In production, use YouTube Data API 668 669 // Try to extract playlist ID for uploads 670 if (preg_match('/list=([a-zA-Z0-9_-]+)/', $channel_url, $match)) { 671 $playlist_id = $match[1]; 672 // This could be used with YouTube API to get playlist items 673 } 674 675 // Fallback: Return single video metadata if URL contains video 676 if (strpos($channel_url, 'watch?v=') !== false) { 677 $context_metadata = $this->get_video_context_metadata($channel_url); 678 if (!empty($context_metadata)) { 679 $videos[] = $context_metadata; 680 } 681 } 682 683 return $videos; 684 } 685 686 /** 687 * Get videos from Vimeo channel/user 688 * 689 * @param string $channel_url Channel URL 690 * @param int $max_videos Maximum videos 691 * @return array 692 */ 693 private function get_vimeo_channel_videos($channel_url, $max_videos) { 694 $videos = array(); 695 696 // Try to get videos from channel 697 if (preg_match('/vimeo\.com\/channels\/([a-zA-Z0-9_-]+)/', $channel_url, $match)) { 698 $channel_id = $match[1]; 699 // In production, use Vimeo API to get channel videos 700 } 701 702 // Fallback: Return single video metadata if URL contains video 703 if (preg_match('/vimeo\.com\/(\d+)/', $channel_url, $match)) { 704 $video_id = $match[1]; 705 $video_url = 'https://vimeo.com/' . $video_id; 706 $context_metadata = $this->get_video_context_metadata($video_url); 707 if (!empty($context_metadata)) { 708 $videos[] = $context_metadata; 709 } 710 } 711 712 return $videos; 713 } 714 715 /** 716 * Get videos from TikTok profile 717 * 718 * @param string $profile_url TikTok profile URL 719 * @param int $max_videos Maximum videos 720 * @return array 721 */ 722 private function get_tiktok_profile_videos($profile_url, $max_videos) { 723 $videos = array(); 724 725 // Try to get videos from profile 726 if (preg_match('/tiktok\.com\/@([a-zA-Z0-9_.]+)/', $profile_url, $match)) { 727 $username = $match[1]; 728 // In production, use TikTok API to get user videos 729 } 730 731 // Fallback: Return single video metadata if URL contains video 732 if (strpos($profile_url, '/video/') !== false) { 733 $context_metadata = $this->get_video_context_metadata($profile_url); 734 if (!empty($context_metadata)) { 735 $videos[] = $context_metadata; 736 } 737 } 738 739 return $videos; 740 } 354 741 } -
technodrome-ai-content-assistant/trunk/readme.txt
r3432273 r3434643 1 1 === Technodrome AI Content Assistant === 2 2 Contributors: technodrome 3 Tags: ai, content, generat ion, openai, writing3 Tags: ai, content, generator, openai, claude, gemini, wordpress 4 4 Requires at least: 6.0 5 5 Tested up to: 6.9 6 6 Requires PHP: 8.0 7 Stable tag: 3.8.37 Stable tag: 4.0.0 8 8 License: GPL v2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 10 11 Professional AI-powered content generation with multiple AI providers, custom writing rules, and 40+ language support.11 Advanced AI content generation plugin with multiple AI providers, profile system, layout templates, content rules, and AI + Video Context generation modes. 12 12 13 13 == Description == 14 14 15 Technodrome AI Content Assistant is a powerful content generation plugin that integrates 9 AI providers to help you create high-quality blog posts, pages, and content for your site. This plugin is not affiliated with OpenAI, Anthropic, Google, DeepSeek, Cohere, Groq, Together AI, or Mistral.15 Technodrome AI Content Assistant is a powerful WordPress plugin that enables you to generate high-quality AI-powered content using multiple AI providers including OpenAI (GPT), Anthropic (Claude), Google (Gemini), DeepSeek, Cohere, Groq, Together AI, and Mistral AI. 16 16 17 = Key Features = 17 ### Key Features 18 18 19 * **9 AI Providers**: OpenAI GPT-4/5, Anthropic Claude 4.5, Google Gemini 3, DeepSeek V3.5, Cohere Command, Groq Qwen, Together AI, Mistral, Demo Mode 20 * **40+ Languages**: Generate content in English, Spanish, French, German, Italian, Portuguese, Russian, Chinese, Japanese, Korean, Arabic, Hindi, and 30+ more languages 21 * **Content Types**: Blog posts, pages, product descriptions, reviews, tutorials, news articles 22 * **Writing Tones**: Professional, friendly, casual, formal, enthusiastic, informative 23 * **Word Count Options**: 300, 500, 800, 1200 words 24 * **Demo Mode**: Template-based generation without API keys 25 * **Full Integration**: Seamless integration with categories and post management 26 * **Smart Language Selector**: Search by language name, country, or flag 27 * **Content History**: Track and manage all generated content 28 * **Bulk Generation**: Generate multiple articles at once with queue management (v3.2.0+) 19 * **Multiple AI Providers**: Choose from 8+ AI providers with access to the latest models (GPT-5, Claude 4, Gemini 2.5, DeepSeek R1, etc.) 20 * **Profile System**: Save and manage multiple content generation profiles with custom settings 21 * **Layout Templates**: 6 professional templates with drag-and-drop builder for custom layouts 22 * **Content Rules**: Create custom rules for headings, guidelines, and writing style 23 * **40+ Languages**: Generate content in any language with native-quality output 24 * **Multi-Provider AI Models**: Access hundreds of AI models from different providers 25 * **AutoSave Revolution**: Automatic profile saving and content history 29 26 30 = AI Provider Support = 27 ### NEW in v4.0.0 - AI + Video Context Generation 31 28 32 * **OpenAI**: GPT-5.2, GPT-5.1, GPT-4.1, GPT-4.1-Mini, GPT-4o, GPT-4-Turbo, O3, O1, DALL-E 3 (Images) 33 * **Anthropic**: Claude 4.5 Opus, Claude 4.5 Sonnet, Claude 4.5 Haiku, Claude 3.5 Sonnet, Claude 3.5 Haiku 34 * **Google**: Gemini 3 Pro, Gemini 2.5 Pro, Gemini 2.5 Flash Lite, Gemini 2.0 Flash Lite, Gemini 1.5 Pro 35 * **DeepSeek**: DeepSeek V3.5, DeepSeek V3.2, DeepSeek V3, DeepSeek R1, DeepSeek R1 Distill 36 * **Cohere**: Command A 03, Command R+, Command R, Command Light 37 * **Groq**: Qwen QWQ 32B, Llama 3.3 70B, DeepSeek R1 Distill Llama, Mixtral 8x7B 38 * **Together AI**: Nvidia Nemotron 3 Nano (and more) 39 * **Mistral**: Mistral Large, Mistral Small, Codestral, Mistral Nemo, Ministral 8B 40 41 = Use Cases = 42 43 * **Blog Management**: Generate consistent blog posts and maintain brand voice 44 * **E-commerce**: Create product descriptions and marketing copy 45 * **Multilingual Sites**: Generate content in 40+ languages for international markets 46 * **Content Marketing**: Social media content, newsletters, landing pages 47 * **SEO**: Optimized articles and meta descriptions 48 49 = Free vs Pro = 50 51 **Free Version includes:** 52 * Unlimited demo mode generation 53 * All content types and tones 54 * Basic AI provider support 55 * 40+ language support 56 * Full integration 57 * Content management 58 * Generation history 59 60 **Pro Version (Coming Soon):** 61 * Unlimited content rules 62 * Advanced AI templates 63 * Enhanced bulk generation features 64 * Custom brand voice training 65 * Priority AI processing 66 * Advanced analytics 67 * Team collaboration 68 * Premium support 29 * **AI + Video URL Context (PREMIUM)**: Generate articles from single video URLs (YouTube, Vimeo, TikTok) 30 * **AI + Video Channel Context (PREMIUM)**: Generate articles from entire video channels 31 * **Video Slot Transformation**: Smart slot locking based on selected generation mode 32 * **Compact Notification**: Clear UI feedback when video context mode is active 69 33 70 34 == Installation == 71 35 72 = Automatic Installation = 36 1. Upload the plugin files to the `/wp-content/plugins/technodrome-ai-content-assistant` directory, or install the plugin through the WordPress plugins screen directly. 37 2. Activate the plugin through the 'Plugins' screen in WordPress. 38 3. Configure your AI provider API keys in the plugin settings. 39 4. Start generating AI content! 73 40 74 1. Go to Plugins > Add New in your admin area 75 2. Search for "Technodrome AI Content Assistant" 76 3. Click Install and then Activate 77 4. Go to Technodrome AI Content Assistant in the admin menu to start generating content 41 == Changelog == 78 42 79 = Manual Installation = 43 = 4.0.0 (2026-01-07) = 44 * **NEW**: Added "AI + Video URL Context (PREMIUM)" generation mode 45 * **NEW**: Added "AI + Video Channel Context (PREMIUM)" generation mode 46 * **NEW**: Video slot transformation system - slots automatically lock/unlock based on generation mode 47 * **NEW**: Compact notification above "Add Content Title" field when video context mode is active 48 * **NEW**: Video URL Context activates Slot 1, Video Channel Context activates Slot 2 49 * **IMPROVED**: Integration with Joomla-style video context patterns 50 * **IMPROVED**: Inline script compatibility with transformation mode 51 * **FIXED**: Video context mode detection in generate-button.js 52 * **FIXED**: CSS styles for video context notification 53 * **UPDATED**: Version to 4.0.0 80 54 81 1. Download the plugin ZIP file 82 2. Go to Plugins > Add New > Upload Plugin 83 3. Choose the ZIP file and click Install Now 84 4. Activate the plugin 85 5. Access via Technodrome AI Content Assistant in admin menu 55 = 3.8.3 (2025-12-15) = 56 * License Key Format Normalization 57 * Critical Bug Fixes 58 * Improved API stability 86 59 87 = Requirements = 60 = 3.8.2 (2025-11-20) = 61 * Enhanced AI model compatibility 62 * Performance improvements 88 63 89 * WordPress 6.0 or higher 90 * PHP 8.0 or higher 91 * MySQL 5.7 or higher 92 * Editor role or higher for content generation 64 = 3.8.1 (2025-11-01) = 65 * Fixed language handler integration 66 * Added new AI models 67 68 = 3.8.0 (2025-10-15) = 69 * Added 40+ language support 70 * Enhanced content rules system 71 * New layout templates 72 73 == Upgrade Notice == 74 75 = 4.0.0 = 76 This update adds AI + Video Context generation modes. Please test in a staging environment before upgrading production sites. 93 77 94 78 == Frequently Asked Questions == 95 79 96 = Do I need API keys to use this plugin? =80 = Do I need an API key? = 97 81 98 No! The plugin includes a demo mode that works without any API keys. For advanced features and unlimited generation, you can add API keys from supported providers. 82 Yes, you need an API key from your chosen AI provider (OpenAI, Anthropic, Google, etc.). 83 84 = Is this plugin free? = 85 86 The core plugin is free. Video URL and Video Channel Context modes require a PREMIUM license. 99 87 100 88 = Which AI providers are supported? = 101 89 102 We support OpenAI (ChatGPT), Anthropic (Claude), Google (Gemini), DeepSeek, and Cohere. Each provider offers different models and pricing options. 103 104 = How many languages are supported? = 105 106 The plugin supports 40+ languages including English, Spanish, French, German, Italian, Portuguese, Russian, Chinese, Japanese, Korean, Arabic, Hindi, and many more. 107 108 = Is the generated content SEO-optimized? = 109 110 Yes, the plugin generates SEO-friendly content with proper structure, headings, and meta descriptions. You can also specify SEO requirements in your content prompts. 111 112 = Can I edit the generated content? = 113 114 Absolutely! All generated content can be edited in the standard editor. The plugin creates regular posts/pages that you have full control over. 115 116 = Is my data secure? = 117 118 Yes. Your site communicates directly with AI providers. We don't store or access your API communications. All API keys are stored securely in your database. 119 120 = What about API costs? = 121 122 API usage costs depend on your chosen provider. Most providers offer free tier credits, and costs are typically very low for content generation. 123 124 = Can I use custom writing styles? = 125 126 The Pro version (coming soon) will include custom content rules and brand voice training. The free version offers multiple built-in writing tones. 90 OpenAI (GPT), Anthropic (Claude), Google (Gemini), DeepSeek, Cohere, Groq, Together AI, and Mistral AI. 127 91 128 92 == Screenshots == 129 93 130 1. Main dashboard with content generation options 131 2. AI provider selection and settings 132 3. Language selector with 40+ options 133 4. Generated content preview and editing 134 5. Content history and management 135 6. Settings and configuration panel 94 1. Dashboard overview with generation controls 95 2. Layout templates with drag-and-drop builder 96 3. AI provider and model selection 97 4. Content rules configuration 98 5. Generated content history 136 99 137 == Changelog==100 == License == 138 101 139 = 3.5.4 - 2026-01-04 - Credit Toggle & Auto-Reset on Empty/Invalid License 140 * **NEW FEATURE: Credit Toggle** - PRO/PREMIUM users can optionally hide Technodrome credit footer from generated articles 141 * **Free users:** Credit footer remains mandatory (toggle disabled) 142 * **Pro/Premium users:** Can toggle credit footer YES/NO via new footer control 143 * **NEW FEATURE: Auto-Reset on Empty/Invalid License Key** - Automatically resets to FREE tier when: 144 * License key field is emptied and saved 145 * License key is invalid and Save License is clicked 146 * License validation fails for any reason 147 * **IMPROVEMENT:** Better user experience for license management - no more stuck premium state 148 * **SECURITY:** Prevents users from keeping premium features with invalid/empty keys 149 150 = 3.5.3 - 2025-12-28 - License Expiration Auto-Reset 151 * ** NEW FEATURE: License Expiration Auto-Reset *** 152 * ** Automatic detection of expired licenses on page load 153 * **Auto-resets to free tier (Profile 1) when license expires 154 * **Clears all PRO features and premium settings 155 * **Adds prominent expiration banner in dashboard 156 * **Seamless user experience with minimal disruption 157 158 = 3.5.2 - 2025-12-18 = 159 * **CRITICAL BUG FIX:** Fixed API model loading logic - API models are now always used when available 160 * **NEW FEATURE:** Clear Model Cache button in dashboard footer for instant cache refresh 161 * **SECURITY:** Fixed nonce verification for cache clearing AJAX handler 162 * **USER BENEFIT:** No more waiting 24 hours for new models to appear - click button to refresh instantly 163 164 = 3.5.0 - 2025-12-16 = 165 * **CRITICAL FIX:** Anthropic API Key Validation - Changed from expensive API calls to format-only validation 166 * **CRITICAL FIX:** Image Generation Empty Fragment Warning - Fixed DOMNode errors and added proper validation 167 * **NEW:** Support for 9 AI providers (Demo, OpenAI, Anthropic, Google, DeepSeek, Cohere, Groq, Together, Mistral) 168 * **IMPROVEMENT:** All providers working correctly with proper error handling and validation 169 170 = 3.3.3 - 2025-12-13 = 171 * **FIX:** OpenAI Model Capability Detection - Fixed image generation toggle for correct models 172 * **TECHNICAL:** Removed invalid model 'gpt-4' from image capable models list 173 174 = 3.3.2 - 2025-12-13 = 175 * **FIX:** Photo Positions Null Container Error - Fixed crashes when generating articles without photo slots 176 * **IMPROVEMENT:** Articles now generate successfully with or without template photo slots 177 178 = 3.3.1 - 2025-12-13 = 179 * **CRITICAL FIX:** API Key Sanitization Bug - API keys were being corrupted by sanitize_text_field() 180 * **FIX:** Fixed API key validation regex patterns for all providers 181 * **IMPROVEMENT:** All API keys now work correctly without corruption 182 183 = 3.3.0 - 2025-12-13 = 184 * **NEW FEATURE:** AI Image Generation with DALL-E 3 - Automatically generate featured images for articles 185 * **NEW:** AI Image Toggle in Generate tab with model capability detection 186 * **NEW:** WordPress Media Library Integration - Generated images saved automatically 187 * **NEW:** Visual capability indicators (✓ for supported, ✗ for unsupported models) 188 * **SUPPORTED:** All OpenAI models support AI image generation with DALL-E 3 189 * **LICENSE:** AI Image Generation is FREE for all users (not PREMIUM) 190 191 = 3.2.9 - 2025-12-09 = 192 * **NEW FEATURE:** Schedule Publishing Template Support - Scheduled articles now use selected template 193 * **FIX:** Schedule Publishing processes images based on template layout 194 * **FIX:** Fixed category assignment for scheduled articles 195 * **IMPROVEMENT:** Removed duplicate Category dropdown from form 196 197 = 3.2.7 - 2025-11-22 = 198 * **PRODUCTION CLEANUP:** Complete cleanup of debug code and development artifacts for production deployment 199 * **CLEANUP:** Removed all `error_log()`, `print_r()`, `var_dump()` debug functions from PHP files 200 * **CLEANUP:** Removed suvišni `console.log()` debug pozivi sa emoji oznakama iz JavaScript fajlova 201 * **CLEANUP:** Očišćeni svi `[DEBUG]` označeni pozivi iz advanced-template.js (50+ linija) 202 * **OPTIMIZATION:** Code optimization for better performance and WordPress standards compliance 203 * **PREPARATION:** Priprava za buduće radove na vizuelnom poboljšanju templatea 1-6 204 * **PREPARATION:** Priprava za fix koda generisanja članaka sa slikama i videom 205 * **TECHNICAL:** Kod je sada production-ready bez suvišnih debug izlaza 206 207 = 3.2.6 - 2025-11-20 = 208 * **CRITICAL FIX:** UTF-8 Encoding Issue - Resolved critical bug where Serbian Cyrillic characters were appearing garbled in generated articles (e.g., "oÅ¡aniÄka Banja" instead of "Jošanička Banja") 209 * **FIX:** Removed problematic `mb_convert_encoding()` functions that were converting UTF-8 text to HTML entities and back, causing character corruption 210 * **FIX:** Updated DOMDocument handling in content generator to preserve original UTF-8 encoding without unnecessary conversions 211 * **FIX:** Removed UTF-8 encoding conversions from all AI provider classes (OpenAI, Anthropic, DeepSeek, Cohere) 212 * **IMPROVEMENT:** Serbian and other non-Latin characters now display correctly in both demo and AI-generated content 213 * **IMPROVEMENT:** Video embed codes now maintain proper encoding in generated articles 214 * **NEW FEATURE:** Global Video System - Implemented new global video management with 2 video slots that can be added to Custom Template 6 215 * **NEW FEATURE:** Video Slot Integration - Videos are now stored globally and can be referenced by slot number in Template 6 Custom Builder 216 * **NEW FEATURE:** Auto-Save Video - Videos automatically save when user clicks outside of input field (blur event) 217 * **NEW FEATURE:** Video Platform Support - Supports YouTube, Vimeo, TikTok, X/Twitter, and Instagram with automatic embed conversion 218 * **TECHNICAL:** Simplified content processing to use WordPress's built-in UTF-8 handling instead of manual encoding conversions 219 220 = 3.2.2 - 2025-10-14 = 221 * **NEW:** Added full interface translations for Spanish (es_ES), Mexican Spanish (es_MX), French (fr_FR), Serbian (sr_RS), Russian (ru_RU), German (de_DE), Japanese (ja), and Chinese (zh_CN). 222 * **IMPROVEMENT:** The plugin interface will now automatically switch to the user's selected WordPress language. 223 * **IMPROVEMENT:** If a translation is missing for a specific string, it will correctly fall back to English. 224 225 = 3.2.1 - 2025-10-13 = 226 * **TECHNICAL:** Name postions of Templates 1-6 227 228 = 3.2.0 - 2025-10-12 = 229 * **MAJOR FEATURE:** Bulk Article Generation - generate multiple articles at once with intelligent queue management 230 * **NEW:** Batch processing - process 1-5 articles simultaneously in configurable batches 231 * **NEW:** Real-time queue management with Pause/Resume/Cancel controls 232 * **NEW:** Bulk analytics dashboard showing Total/Completed/Processing/Failed articles with live statistics 233 * **NEW:** Progress tracking with visual progress bar and estimated completion time 234 * **NEW:** Topic input with multi-line textarea (one topic per line) and real-time counter 235 * **NEW:** Smart delays - configurable delay between batches (0-60 seconds) to prevent API throttling 236 * **NEW:** Error handling - failed articles are marked and tracked, processing continues with remaining items 237 * **IMPROVEMENT:** Queue system with complete state management (pending/processing/completed/failed/cancelled statuses) 238 * **IMPROVEMENT:** Real-time updates with status icons (⏳ Pending, 🔄 Processing, ✅ Completed, ❌ Failed, ⛔ Cancelled) 239 * **IMPROVEMENT:** Responsive two-column layout (input + queue/analytics) with mobile support 240 * **IMPROVEMENT:** Color-coded status indicators with smooth animations and pulse effects 241 * **TECHNICAL:** Uses existing taics_generate_content AJAX handler - no new PHP/database required 242 * **TECHNICAL:** Profile-based generation - uses Active Profile settings for all articles 243 * **USE CASES:** Content calendar filling, topic research batch, multi-language content, SEO creation, emergency publishing 244 245 = 3.1.1 - 2025-10-11 = 246 * **CRITICAL FIX:** Resolved Template 6 canvas persistence issue - canvas data now properly loads even when plugin initializes on different tabs 247 * **FIX:** Added placeholder object creation for TAICS_Advanced_Template when module hasn't loaded yet, preventing canvas data loss 248 * **IMPROVEMENT:** Enhanced profile-buttons.js to always set canvasData in Advanced Template object regardless of current tab 249 * **TECHNICAL:** Improved canvas data initialization logic to handle asynchronous module loading scenarios 250 251 = 3.1.0 - 2025-10-10 = 252 * **NEW FEATURE:** Structure Name field added to Content Rules tab for organizing custom content structures 253 * **NEW FEATURE:** Writing Guidelines editor - define custom writing instructions, style guidelines, and formatting rules 254 * **NEW FEATURE:** Web Sources input - add up to 5 URLs as reference sources for AI content generation 255 * **NEW FEATURE:** Structure Preview panel - real-time preview of headings structure with visual formatting 256 * **NEW FEATURE:** Web Sources validation with traffic light system (red/yellow/green) for URL status 257 * **IMPROVEMENT:** Enhanced Content Structure Builder with complete workflow: Name → Guidelines → Sources → Headings → Preview 258 * **IMPROVEMENT:** Unicode character support - Serbian Cyrillic and special characters now save correctly (fixed JSON_UNESCAPED_UNICODE issue) 259 * **IMPROVEMENT:** AI content generation quality - increased max_tokens from 2x to 3x word count for all 5 AI providers (OpenAI, Anthropic, Google, DeepSeek, Cohere) 260 * **IMPROVEMENT:** Template 6 Custom Builder persistence - canvas data now properly loads when plugin initializes, even before Layout Tab is accessed 261 * **FIX:** Resolved Template 6 canvas preview loss after plugin reload - added initialization logic to restore canvas elements 262 * **FIX:** Fixed profile save/load for structure_name field - properly sanitized and stored in database 263 * **FIX:** Corrected headings array handling - fixed sanitization to support both array and string formats 264 * **FIX:** WordPress Coding Standards compliance - added phpcs:disable comments for legitimate third-party embed scripts (TikTok, Twitter, Instagram) 265 * **TECHNICAL:** Added JSON_UNESCAPED_UNICODE flag to profile manager for proper UTF-8 character storage 266 * **TECHNICAL:** Enhanced advanced-template.js initialization to check for pre-loaded canvas data 267 * **TECHNICAL:** Improved profile-buttons.js to properly load and restore all Content Rules fields 268 269 = 3.0.0 - 2025-10-17 = 270 * **MAJOR RELEASE:** Introduced Template 6 - Custom Builder with advanced drag & drop canvas 271 * **NEW FEATURE:** Custom Builder allows users to construct custom article layouts by dragging and dropping elements (AI Text, Photos, Custom Text, Video Embed, iFrame Embed) 272 * **NEW FEATURE:** Video Embed element with support for YouTube, Vimeo, TikTok, Twitter/X, and Instagram embeds 273 * **NEW FEATURE:** Custom Text element for inserting user-written content blocks with HTML formatting support 274 * **NEW FEATURE:** iFrame Embed element for embedding external content with customizable height 275 * **NEW FEATURE:** Canvas persistence - Template 6 layouts are saved in user profiles and persist across sessions 276 * **IMPROVEMENT:** Enhanced content generation progress notifications - added 7 detailed progress steps (from 3) to keep users informed during generation 277 * **IMPROVEMENT:** Reorganized Layout Templates grid - Templates 1-3 in row 1, Templates 4-5 in left column, Template 6 as large 2x2 canvas with Element Palette always visible below 278 * **IMPROVEMENT:** Template 6 canvas data is properly sanitized and stored in profile with security measures (URL sanitization, HTML filtering) 279 * **FIX:** Resolved critical bug where Template 6 canvas elements were not being saved to database due to missing sanitization in Profile Manager 280 * **FIX:** Fixed canvas initialization timing issues - added retry logic to ensure canvas loads properly after profile switch 281 * **FIX:** Removed all debug error_log calls to fully comply with WordPress Plugin Check standards (0 warnings) 282 * **TECHNICAL:** Added `advanced_template_canvas` field to profile structure with dedicated sanitization method 283 * **TECHNICAL:** Implemented proper canvas data flow: JavaScript collection → AJAX → Profile Manager → Database → Content Generator 284 * **SECURITY:** All canvas elements are sanitized: URLs with esc_url_raw(), HTML content with wp_kses_post(), text fields with sanitize_text_field() 285 286 = 2.7.1 - 2025-10-16 = 287 * **NEW FEATURE:** Added photo link functionality - images in articles can now be clickable and redirect to custom URLs 288 * **IMPROVEMENT:** Photo links persist across profile switches and tab navigation 289 * **IMPROVEMENT:** Photo links are properly saved in user metadata and sent during content generation 290 * **FIX:** Removed debug error_log calls to comply with WordPress Plugin Check requirements 291 * **IMPROVEMENT:** Images with links are now wrapped in anchor tags with target="_blank" and proper security attributes 292 293 = 2.6.1 - 2024-10-03 = 294 * **Enhancement:** Significantly improved the user experience in the "History" tab. 295 * **Feature:** After generating an article, the user is now automatically switched to the History tab. 296 * **Feature:** The History tab now automatically refreshes with the latest content without needing a page reload. 297 * **Feature:** Deleting articles from the History tab is now instant (via AJAX) and no longer opens a new window. The deleted item is removed from the list with an animation. 298 * **Fix:** Resolved a bug where the history list wouldn't update correctly after specific actions. 299 300 = 2.6.0 - 2024-09-30 = 301 * **CRITICAL FIX:** Implemented a robust HTML DOM parser (`DOMDocument` with `XPath`) for image insertion. This resolves the critical bug where images were not appearing in generated articles by correctly preserving all HTML tags (headings, lists, etc.) during manipulation. 302 * **IMPROVEMENT:** Refactored the JavaScript `setValue()` function for photo positions to handle multiple data formats (Array and Object), preventing errors when loading photo data from different sources (user meta vs. AJAX responses). 303 * **IMPROVEMENT:** Corrected the profile loading logic (`loadLayoutTemplateData`) to no longer interfere with photo selections, reinforcing that photos are dynamic content separate from static profiles. 304 * **FIX:** Resolved a bug where the `cleanup()` function in the photo management module was incorrectly clearing selected photos during tab switching, causing the generation process to fail. Photo data now persists correctly. 305 306 = 2.5.0 - 2024-09-29 = 307 * **NEW FEATURE:** Added "Schedule Publishing" for PRO and PREMIUM users, allowing automated content generation and publishing at a future date and time. 308 * **IMPROVEMENT:** Reworked the scheduler statistics logic to correctly filter and display user-specific scheduled articles. 309 * **FIX:** Resolved a critical bug where scheduled articles were not appearing in the schedule list due to incorrect user ID filtering. 310 * **SECURITY:** Hardened the AI provider class by adding escaping to error message outputs, passing WordPress Plugin Check requirements. 311 312 = 2.4.2 - 2024-09-29 = 313 * **UI IMPROVEMENT:** Enhanced the user interface for the "Add Content Title" field for better clarity and usability. 314 * **UI IMPROVEMENT:** Redesigned the "Generate Content" button with improved visual feedback during the content generation process. 315 316 = 2.4.0 - 2024-09-28 = 317 * **REFACTOR:** Reworked the entire content generation logic. The system now exclusively uses settings from the saved active profile, making generation more reliable and predictable. 318 * **FIX:** Resolved a series of bugs that prevented the "Generate Content" button from functioning correctly under various conditions. 319 * **FIX:** Corrected the data flow for API keys and AI model settings, ensuring the correct provider is used. 320 * **IMPROVEMENT:** Simplified the AJAX request for content generation, improving security and performance. 321 322 = 2.3.0 - 2025-09-29 = 323 * **Improvement:** Added a new 'Default Tone' selector to the Generate tab, allowing users to set a consistent writing tone across all generated content. 324 * **Fix:** Resolved a persistent issue where notifications would occasionally appear duplicated upon loading the dashboard. 325 * **Fix:** Addressed a CSS bug causing a "not-allowed" cursor to appear incorrectly over various input fields. 326 * **Fix:** Corrected several visual and functional inconsistencies with Dark Mode, particularly within the Generate tab. 327 * **UI:** Implemented minor visual enhancements to the header and footer for a cleaner user experience. 328 * **Minor bug fixes 329 330 = 2.2.5 - 2025-09-28 = 331 * **UI:** Minor cosmetic adjustments and UI improvements across the dashboard. 332 333 = 2.2.1 - 2025-09-28 = 334 * **Fix:** Corrected a CSS issue where a "not-allowed" cursor was incorrectly displayed on all input fields. Cursors now display correctly (text for inputs, pointer for selects). 335 336 = 2.2.0 - 2025-09-28 = 337 * **Improvement:** Enhanced the footer UI with real-time information about the currently active profile and plugin usage statistics. 338 * **Fix:** Corrected an issue where the plugin version was not dynamically displayed in the header. The version now correctly shows 2.2.0. 339 * **Fix:** Hardened the plugin against various WordPress Plugin Check warnings, including output escaping, nonce sanitization, and fixing the stable tag mismatch. 340 341 = 2.1.0 - 2025-09-28 = 342 * **Improvement:** Overhauled the license validation system to be more robust and secure. All AJAX calls now use a unified nonce system, resolving critical `403 Forbidden` errors and `Invalid nonce` issues. 343 * **Improvement:** Hardened the plugin against double-loading errors by implementing singleton initialization patterns, resolving `Cannot redeclare function` fatal errors. 344 * **Fix:** Corrected an issue where the plugin version was hardcoded in the header and not dynamically displayed. The version now correctly shows 2.1.0. 345 * **Fix:** Removed a duplicate and non-functional AJAX handler for license validation that was causing conflicts. 346 347 = 2.0.0 = 348 * Major refactor and rewrite for WordPress plugin compatibility. 349 * Modularized architecture with separated header, footer, dashboard modules. 350 * Added toast notification system with global header visibility. 351 * AJAX nonce fixes for security and functionality. 352 * Fixed various JS initialization and AJAX bugs. 353 * Removed unused or missing files and dependencies. 354 * Improved UI/UX responsiveness and styling. 355 * Removed shared.css enqueue due to lack of usage. 356 * Deprecated old Joomla code references. 357 = 1.0.0 = 358 * Initial release 359 * Multiple AI provider support (OpenAI, Anthropic, Google, DeepSeek, Cohere) 360 * 40+ language support with smart language selector 361 * Demo mode for testing without API keys 362 * Content types: posts, pages, products, reviews, tutorials, news 363 * Writing tones: professional, friendly, casual, formal, enthusiastic, informative 364 * Word count options: 300, 500, 800, 1200 words 365 * Full category integration 366 * Content history and management 367 * Secure API key storage 368 * Mobile-responsive admin interface 369 370 == Upgrade Notice == 371 372 = 1.0.0 = 373 Initial release of AI Content Assistant. Start generating high-quality content with multiple AI providers and 40+ language support. 374 375 == Third-Party Services == 376 377 This plugin integrates with the following third-party services: 378 379 = OpenAI = 380 * Service: https://platform.openai.com 381 * Privacy Policy: https://openai.com/privacy/ 382 * Terms of Service: https://openai.com/terms/ 383 384 = Anthropic = 385 * Service: https://console.anthropic.com 386 * Privacy Policy: https://www.anthropic.com/privacy 387 * Terms of Service: https://www.anthropic.com/terms 388 389 = Google AI = 390 * Service: https://ai.google.dev 391 * Privacy Policy: https://policies.google.com/privacy 392 * Terms of Service: https://policies.google.com/terms 393 394 = DeepSeek = 395 * Service: https://platform.deepseek.com 396 * Privacy Policy: https://www.deepseek.com/privacy 397 * Terms of Service: https://www.deepseek.com/terms 398 399 = Cohere = 400 * Service: https://dashboard.cohere.ai 401 * Privacy Policy: https://cohere.ai/privacy 402 * Terms of Service: https://cohere.ai/terms 403 404 Note: API keys and content generation requests are sent directly to these services. Please review their privacy policies and terms of service before use. 405 406 == Support == 407 408 For support, documentation, and feature requests: 409 410 * Website: https://technodrome.nasrpskom.com 411 * Support Forum: Use the support tab on this plugin page 412 * Documentation: Included in plugin admin area 413 414 == Credits == 415 416 Developed by Technodrome Team 417 Special thanks to the AI community and all contributors. 102 This plugin is licensed under GPL v2 or later. See LICENSE file for details. -
technodrome-ai-content-assistant/trunk/technodrome-ai-content-assistant.php
r3432273 r3434643 3 3 * Plugin Name: Technodrome AI Content Assistant 4 4 * Plugin URI: https://technodrome.org/ai-content-assistant 5 * Description: Advanced AI content generation plugin with multiple AI providers, profile system, layout templates, and content rules for WordPress. v 3.8.3 - License Key Format Normalization & Critical Bug Fixes.6 * Version: 3.8.35 * Description: Advanced AI content generation plugin with multiple AI providers, profile system, layout templates, and content rules for WordPress. v4.0.0 - AI + Video URL/Channel Context Generation Modes. 6 * Version: 4.0.0 7 7 * Author: Technodrome Team 8 8 * Author URI: https://technodrome.org … … 30 30 31 31 // Plugin constants 32 define('TAICS_VERSION', ' 3.8.3'); // License Key Format Normalization & Critical Bug Fixes32 define('TAICS_VERSION', '4.0.0'); // AI + Video URL/Channel Context Generation Modes 33 33 define('TAICS_PLUGIN_FILE', __FILE__); 34 34 define('TAICS_PLUGIN_DIR', plugin_dir_path(__FILE__)); … … 286 286 'content-rules' => 'content-rules-tab/content-rules.css', 287 287 'layout-templates' => 'layout-templates-tab/layout-templates.css', 288 'video-manager' => 'layout-templates-tab/video-manager.css', 288 289 'extras' => 'extras-tab/extras.css', 289 290 'history' => 'history-tab/history.css' … … 325 326 'category-article' => 'generate-tab/category-article.js', 326 327 'generation-mode' => 'generate-tab/generation-mode.js', 328 'video-context-mode' => 'generate-tab/video-context-mode.js', 327 329 'ai-provider-select' => 'generate-tab/ai-provider-select.js', 328 330 'ai-image-toggle' => 'generate-tab/ai-image-toggle.js', // AI Image Generation toggle (v3.3.0) … … 337 339 'photo-positions' => 'layout-templates-tab/photo-positions.js', 338 340 'video-manager' => 'layout-templates-tab/video-manager.js', 341 'video-slot-transformation' => 'layout-templates-tab/video-slot-transformation.js', 339 342 'advanced-template' => 'layout-templates-tab/advanced-template.js', 340 343 … … 386 389 'taics-photo-positions', // NOVO 387 390 'taics-video-manager', // NOVO 391 'taics-video-slot-transformation', // NOVO - Video slot transformation 388 392 'taics-schedule-picker', 389 393 'taics-bulk-generator',
Note: See TracChangeset
for help on using the changeset viewer.