Plugin Directory

Changeset 3434643


Ignore:
Timestamp:
01/07/2026 07:20:17 PM (3 months ago)
Author:
technodrome
Message:

Version 4.0.0 new function AI + Video Context generation

Location:
technodrome-ai-content-assistant/trunk
Files:
3 added
12 edited

Legend:

Unmodified
Added
Removed
  • technodrome-ai-content-assistant/trunk/dashboard/modules/generate-tab/generate.php

    r3423160 r3434643  
    247247                            <?php endif; ?>
    248248                        </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>
    249261                    </select>
    250262                    <div class="taics-field-help">
     
    253265                    </div>
    254266                    </div>
    255                     <div class="taics-form-group">
     267
     268                <div class="taics-form-group">
    256269                    <label for="taics-default-tone">
    257270                        📊
  • technodrome-ai-content-assistant/trunk/dashboard/modules/layout-templates-tab/layout-templates.php

    r3431889 r3434643  
    437437jQuery(document).ready(function($) {
    438438    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    }
    439446
    440447    // Function to toggle video input fields
     
    516523    // Direct click handler for video slot cards
    517524    $('.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        }
    518529        if ($(e.target).closest('button, input').length > 0) {
    519530            return;
     
    531542    // Handle "Select Video URL" button clicks
    532543    $('.taics-btn-video').on('click', function(e) {
     544        // Skip if button is disabled
     545        if ($(this).prop('disabled')) {
     546            return;
     547        }
    533548        e.stopPropagation();
    534549        const slotNum = $(this).data('slot');
  • technodrome-ai-content-assistant/trunk/dashboard/modules/layout-templates-tab/video-manager.css

    r3401081 r3434643  
    404404    background-color: #721c24 !important;
    405405}
     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 */
     447body.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
     459body.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 */
     512body.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
     517body.taics-dark-mode .taics-under-construction .taics-video-preview,
     518body.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
     524body.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  
    3333            const publish = $('#taics-publish-toggle').hasClass('taics-toggle-on');
    3434            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) {
    3742                this.showNotification('Please enter a content title (minimum 3 characters).', 'error');
    3843                $('#taics-topic').focus();
     
    7378                this.showNotification('No active profile selected. Please select or create a profile.', 'warning');
    7479                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');
    7587            }
    7688
     
    8092                publish: publish,
    8193                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
    8296                photos: photos, // Add collected photos
    8397                web_sources: webSources, // Add collected web sources
     
    133147                publish: generationData.publish,
    134148                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
    135151                photos: generationData.photos, // Pass photos to backend
    136152                web_sources: generationData.web_sources, // Pass web sources to backend
  • technodrome-ai-content-assistant/trunk/features/footer/profile-buttons.js

    r3421276 r3434643  
    137137                            this.loadCompleteProfileData(this.activeProfile);
    138138                            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);
    139145                        }
    140146
     
    253259                window.TAICS_AI_Provider_Select.syncWithProfileData(profileData);
    254260            }
     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            }
    255266        },
    256267       
     
    336347        },
    337348
    338                 loadLayoutTemplateData: function(layoutTemplate) {
     349        loadLayoutTemplateData: function(layoutTemplate) {
    339350            const templateId = layoutTemplate.template_id || '1';
    340351
  • technodrome-ai-content-assistant/trunk/features/generate-tab/ai-provider-select.js

    r3423160 r3434643  
    288288        updateModelOptions: function(selectedModelId = null) {
    289289            const modelSelect = $('#taics-ai-model');
     290            const currentModel = modelSelect.val(); // Get current model before clearing
     291           
    290292            modelSelect.empty();
    291293
     
    298300                });
    299301
    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) {
    301305                    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);
    302312                } else {
     313                    // No model specified and no current model - use first
    303314                    modelSelect.prop('selectedIndex', 0);
    304315                }
     
    671682
    672683            $('#taics-ai-provider').val(provider);
     684            $('#taics-ai-model').val(model || '');
    673685            $('#taics-api-key').val(apiKey);
    674686
     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();
    675691            this.updateModelOptions(model);
    676692            this.updateProviderDisplay();
  • technodrome-ai-content-assistant/trunk/features/layout-templates-tab/photo-positions.js

    r3421276 r3434643  
    1616        selectedPhotos: {}, // Stores selected photos keyed by position: {1: {id, url, alt}, 2: {...}}
    1717        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
    1819
    1920        init: function() {
     
    2829            this.bindEvents();
    2930
    30             this.loadPhotosFromUserMeta(); // LOAD na startu
     31            this.loadPhotosFromUserMeta(); // LOAD na startu - only if no profile photos yet
    3132            // Listen for template changes to update the "required" status, but not to lock/unlock slots
    3233            $(document).on('taics_template_changed.photo-positions-status', this.updateStatus.bind(this));
     
    211212            }
    212213
    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
    214228            if (Object.keys(this.selectedPhotos).length === 0) {
    215229                console.log('TAICS Photo Positions: No photos selected, loading from user_meta...');
     
    229243
    230244        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           
    231249            this.selectedPhotos = {};
    232250            this.photoLinks = {};
  • technodrome-ai-content-assistant/trunk/includes/class-ajax-handler.php

    r3431889 r3434643  
    4646        add_action('wp_ajax_taics_clear_model_cache', [__CLASS__, 'handle_clear_model_cache']);
    4747        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']);
    4852    }
    4953
     
    6367       
    6468        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']))) {
    6678                wp_send_json_error(esc_html__('Required data missing: Topic or Profile ID is empty.', 'technodrome-ai-content-assistant'));
    6779                return;
     
    7587                'photos'            => [], // Default to empty array
    7688                '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
    7892            ];
    7993
     
    195209                }
    196210            }
    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
    198306            require_once plugin_dir_path(__FILE__) . 'class-content-generator.php';
    199307           
     
    13171425        }
    13181426    }
     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    }
    13191515}
  • technodrome-ai-content-assistant/trunk/includes/class-content-generator.php

    r3431889 r3434643  
    3232     */
    3333    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'])) {
    3540            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            }
    3653        }
    3754
     
    5370        $generation_args['topic'] = sanitize_text_field($args['topic']);
    5471        $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        }
    5584
    5685        // CRITICAL FIX v3.4.2: Merge photos, videos, layout_template, and web_sources from AJAX args
     
    451480        // Correctly map data from the loaded profile structure ($args)
    452481        $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
    466496        );
    467497
     
    552582        }
    553583
    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);
    555619        $post_id = self::create_post($title, $content, $args);
    556620
     
    607671        $prompt = '';
    608672
    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
    611696            $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";
    612697            $prompt .= "TOPIC: {$topic}\n";
     
    842927   
    843928    /**
    844      * Helper: Insert HTML after a node
     929     * Helper: Insert HTML after a node - FIXED for complex HTML like iframes
    845930     */
    846931    private static function insert_after_node($dom, $node, $html) {
    847932        if (!$node) return;
    848933
     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)
    849939        $fragment = $dom->createDocumentFragment();
    850 
    851         // v3.5.0 FIXED: Validate fragment before insertion
    852940        $xml_loaded = $fragment->appendXML($html);
    853941
    854         // Don't insert empty fragments
    855         if ($xml_loaded === false || $fragment->childNodes->length === 0) {
    856             if (defined('WP_DEBUG') && WP_DEBUG) {
    857                 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
    858                 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);
    859947            }
    860948            return;
    861949        }
    862950
    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
    872970     */
    873971    private static function insert_before_node($dom, $node, $html) {
    874972        if (!$node) return;
    875973
     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)
    876979        $fragment = $dom->createDocumentFragment();
    877 
    878         // v3.5.0 FIXED: Validate fragment before insertion
    879980        $xml_loaded = $fragment->appendXML($html);
    880981
    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);
    887984            return;
    888985        }
    889986
    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        }
    891998    }
    892999   
     
    9201027    private static function process_advanced_template_elements($canvas_data, $image_blocks, $ai_content, $videos = array()) {
    9211028        if (empty($canvas_data) || !is_array($canvas_data)) {
     1029            // v4.0.4 FIX: Return original AI content if no canvas data
    9221030            return $ai_content;
    9231031        }
     
    10811189
    10821190        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 '';
    10831208    }
    10841209
     
    12881413    /**
    12891414     * Insert videos into generated content
     1415     * Fixed to avoid duplicate videos in Template 6
    12901416     */
    12911417    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
    12931437        $layout_template = isset($args['layout_template']) ? $args['layout_template'] : [];
    12941438        $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
    12951448        $videos = isset($args['videos']) && is_array($args['videos']) ? $args['videos'] : [];
    12961449
    1297         // Template 1 or no videos
     1450        // Template 1 or no videos - return early
    12981451        if ($template_id == 1 || empty($videos)) {
    12991452            return $content;
     
    13041457        $video_manager = TAICS_Video_Manager::get_instance();
    13051458
    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)
    13071460        foreach ($videos as $slot => $video_data) {
    13081461            $video_url = '';
    13091462            $video_title = '';
    1310             $actual_slot = $slot; // Default to array index
     1463            $actual_slot = $slot;
    13111464
    13121465            // New profile format: array of objects with {slot, platform, video_id, url, thumbnail}
     
    13151468                $video_url = $video_array['url'] ?? '';
    13161469                $video_title = $video_array['title'] ?? '';
    1317                 // v3.4.2: Use slot from video object if available
    13181470                if (isset($video_array['slot'])) {
    13191471                    $actual_slot = intval($video_array['slot']);
     
    13301482
    13311483                if (!empty($video_html)) {
    1332                     // Wrap video in container with proper styling
    13331484                    $video_blocks[$actual_slot] = sprintf(
    13341485                        '<div class="taics-video-container">%s</div>',
     
    13441495        }
    13451496
    1346         // Sort videos by slot number
     1497        // Sort videos by slot number and re-index
    13471498        ksort($video_blocks);
    13481499        $video_blocks = array_values($video_blocks);
    13491500
    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
    13511507        $dom = new DOMDocument('1.0', 'UTF-8');
    13521508        libxml_use_internal_errors(true);
     
    13551511
    13561512        $xpath = new DOMXPath($dom);
    1357 
    1358         // Find all block-level elements (p, h1-h6, ul, ol, blockquote, etc.)
    13591513        $blocks = $xpath->query('//body/*');
    13601514        $total_blocks = $blocks->length;
    13611515
    13621516        if ($total_blocks == 0) {
    1363             // Fallback: simple string insertion
    13641517            return self::insert_videos_simple($content, $video_blocks, $template_id);
    13651518        }
    13661519
    13671520        switch ($template_id) {
    1368             case 2: // Single Video - after first block
     1521            case 2:
    13691522                if (isset($video_blocks[0]) && $blocks->length > 0) {
    13701523                    $first_block = $blocks->item(0);
     
    13731526                break;
    13741527
    1375             case 3: // Two Videos - after first, then middle
     1528            case 3:
    13761529                if (isset($video_blocks[0]) && $blocks->length > 0) {
    13771530                    $first_block = $blocks->item(0);
    13781531                    self::insert_after_node($dom, $first_block, $video_blocks[0]);
    13791532                }
    1380 
    1381                 // Re-query after first insertion
    13821533                $blocks = $xpath->query('//body/*');
    13831534                if (isset($video_blocks[1]) && $blocks->length > 3) {
     
    13871538                break;
    13881539
    1389             case 4: // Three Videos - first, third, middle
     1540            case 4:
    13901541                if (isset($video_blocks[0]) && $blocks->length > 0) {
    13911542                    $first_block = $blocks->item(0);
    13921543                    self::insert_after_node($dom, $first_block, $video_blocks[0]);
    13931544                }
    1394 
    1395                 // Re-query after first insertion
    13961545                $blocks = $xpath->query('//body/*');
    13971546                if (isset($video_blocks[1]) && $blocks->length > 2) {
     
    13991548                    self::insert_after_node($dom, $third_block, $video_blocks[1]);
    14001549                }
    1401 
    1402                 // Re-query after second insertion
    14031550                $blocks = $xpath->query('//body/*');
    14041551                if (isset($video_blocks[2]) && $blocks->length > 4) {
     
    14081555                break;
    14091556
    1410             case 5: // Magazine style - distributed
     1557            case 5:
    14111558                if (isset($video_blocks[0]) && $blocks->length > 0) {
    14121559                    $first_block = $blocks->item(0);
    14131560                    self::insert_after_node($dom, $first_block, $video_blocks[0]);
    14141561                }
    1415 
    14161562                $blocks = $xpath->query('//body/*');
    14171563                $total = $blocks->length;
    1418 
    14191564                if (isset($video_blocks[1]) && $total > 4) {
    14201565                    $pos = floor($total / 3);
     
    14221567                    self::insert_after_node($dom, $target, $video_blocks[1]);
    14231568                }
    1424 
    14251569                $blocks = $xpath->query('//body/*');
    14261570                $total = $blocks->length;
    1427 
    14281571                if (isset($video_blocks[2]) && $total > 6) {
    14291572                    $pos = floor(($total * 2) / 3);
     
    14331576                break;
    14341577
    1435             case 6: // Custom template - use specific video slots
    1436                 // Insert at specific positions based on template logic
     1578            case 6:
     1579                // Template 6 Custom Builder fallback grid - insert at specific positions
    14371580                if (isset($video_blocks[0]) && $blocks->length > 1) {
    1438                     // Video 1 after introduction paragraph
    14391581                    $intro_block = $blocks->item(0);
    14401582                    self::insert_after_node($dom, $intro_block, $video_blocks[0]);
    14411583                }
    1442 
    14431584                $blocks = $xpath->query('//body/*');
    14441585                if (isset($video_blocks[1]) && $blocks->length > 4) {
    1445                     // Video 2 after main content
    14461586                    $main_content_block = $blocks->item(3);
    14471587                    self::insert_after_node($dom, $main_content_block, $video_blocks[1]);
     
    14521592        // Save modified content
    14531593        $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;
    14561604    }
    14571605
  • technodrome-ai-content-assistant/trunk/includes/class-video-manager.php

    r3401081 r3434643  
    352352        }
    353353    }
     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    }
    354741}
  • technodrome-ai-content-assistant/trunk/readme.txt

    r3432273 r3434643  
    11=== Technodrome AI Content Assistant ===
    22Contributors: technodrome
    3 Tags: ai, content, generation, openai, writing
     3Tags: ai, content, generator, openai, claude, gemini, wordpress
    44Requires at least: 6.0
    55Tested up to: 6.9
    66Requires PHP: 8.0
    7 Stable tag: 3.8.3
     7Stable tag: 4.0.0
    88License: GPL v2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 Professional AI-powered content generation with multiple AI providers, custom writing rules, and 40+ language support.
     11Advanced AI content generation plugin with multiple AI providers, profile system, layout templates, content rules, and AI + Video Context generation modes.
    1212
    1313== Description ==
    1414
    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.
     15Technodrome 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.
    1616
    17 = Key Features =
     17### Key Features
    1818
    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
    2926
    30 = AI Provider Support =
     27### NEW in v4.0.0 - AI + Video Context Generation
    3128
    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
    6933
    7034== Installation ==
    7135
    72 = Automatic Installation =
     361.  Upload the plugin files to the `/wp-content/plugins/technodrome-ai-content-assistant` directory, or install the plugin through the WordPress plugins screen directly.
     372.  Activate the plugin through the 'Plugins' screen in WordPress.
     383.  Configure your AI provider API keys in the plugin settings.
     394.  Start generating AI content!
    7340
    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 ==
    7842
    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
    8054
    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
    8659
    87 = Requirements =
     60= 3.8.2 (2025-11-20) =
     61*   Enhanced AI model compatibility
     62*   Performance improvements
    8863
    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 =
     76This update adds AI + Video Context generation modes. Please test in a staging environment before upgrading production sites.
    9377
    9478== Frequently Asked Questions ==
    9579
    96 = Do I need API keys to use this plugin? =
     80= Do I need an API key? =
    9781
    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.
     82Yes, you need an API key from your chosen AI provider (OpenAI, Anthropic, Google, etc.).
     83
     84= Is this plugin free? =
     85
     86The core plugin is free. Video URL and Video Channel Context modes require a PREMIUM license.
    9987
    10088= Which AI providers are supported? =
    10189
    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.
     90OpenAI (GPT), Anthropic (Claude), Google (Gemini), DeepSeek, Cohere, Groq, Together AI, and Mistral AI.
    12791
    12892== Screenshots ==
    12993
    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
     941.  Dashboard overview with generation controls
     952.  Layout templates with drag-and-drop builder
     963.  AI provider and model selection
     974.  Content rules configuration
     985.  Generated content history
    13699
    137 == Changelog ==
     100== License ==
    138101
    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.
     102This 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  
    33 * Plugin Name: Technodrome AI Content Assistant
    44 * 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. v3.8.3 - License Key Format Normalization & Critical Bug Fixes.
    6  * Version: 3.8.3
     5 * 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
    77 * Author: Technodrome Team
    88 * Author URI: https://technodrome.org
     
    3030
    3131// Plugin constants
    32 define('TAICS_VERSION', '3.8.3'); // License Key Format Normalization & Critical Bug Fixes
     32define('TAICS_VERSION', '4.0.0'); // AI + Video URL/Channel Context Generation Modes
    3333define('TAICS_PLUGIN_FILE', __FILE__);
    3434define('TAICS_PLUGIN_DIR', plugin_dir_path(__FILE__));
     
    286286            'content-rules' => 'content-rules-tab/content-rules.css',
    287287            'layout-templates' => 'layout-templates-tab/layout-templates.css',
     288            'video-manager' => 'layout-templates-tab/video-manager.css',
    288289            'extras' => 'extras-tab/extras.css',
    289290            'history' => 'history-tab/history.css'
     
    325326            'category-article' => 'generate-tab/category-article.js',
    326327            'generation-mode' => 'generate-tab/generation-mode.js',
     328            'video-context-mode' => 'generate-tab/video-context-mode.js',
    327329            'ai-provider-select' => 'generate-tab/ai-provider-select.js',
    328330            'ai-image-toggle' => 'generate-tab/ai-image-toggle.js', // AI Image Generation toggle (v3.3.0)
     
    337339            'photo-positions' => 'layout-templates-tab/photo-positions.js',
    338340            'video-manager' => 'layout-templates-tab/video-manager.js',
     341            'video-slot-transformation' => 'layout-templates-tab/video-slot-transformation.js',
    339342            'advanced-template' => 'layout-templates-tab/advanced-template.js',
    340343
     
    386389            'taics-photo-positions', // NOVO
    387390            'taics-video-manager', // NOVO
     391            'taics-video-slot-transformation', // NOVO - Video slot transformation
    388392            'taics-schedule-picker',
    389393            'taics-bulk-generator',
Note: See TracChangeset for help on using the changeset viewer.