From ad4f6c63d57ea61e5e6ba564abc470bf42c71411 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Tue, 28 Oct 2025 15:26:56 +0000 Subject: [PATCH 1/8] ai changes Co-authored-by: Sculptor --- AI_TASKS_IMPLEMENTATION.md | 287 +++++++++++++++++ AI_TASKS_QUICK_START.md | 247 ++++++++++++++ assets/css/ai-task.css | 126 ++++++++ assets/js/recommendations/ai-task.js | 212 ++++++++++++ classes/class-ai-tasks.php | 156 +++++++++ classes/class-base.php | 1 + classes/class-suggested-tasks.php | 35 +- .../suggested-tasks/class-tasks-manager.php | 2 + .../providers/class-ai-task.php | 230 +++++++++++++ .../providers/class-ai-tasks-from-server.php | 302 ++++++++++++++++++ 10 files changed, 1596 insertions(+), 2 deletions(-) create mode 100644 AI_TASKS_IMPLEMENTATION.md create mode 100644 AI_TASKS_QUICK_START.md create mode 100644 assets/css/ai-task.css create mode 100644 assets/js/recommendations/ai-task.js create mode 100644 classes/class-ai-tasks.php create mode 100644 classes/suggested-tasks/providers/class-ai-task.php create mode 100644 classes/suggested-tasks/providers/class-ai-tasks-from-server.php diff --git a/AI_TASKS_IMPLEMENTATION.md b/AI_TASKS_IMPLEMENTATION.md new file mode 100644 index 000000000..d097427d1 --- /dev/null +++ b/AI_TASKS_IMPLEMENTATION.md @@ -0,0 +1,287 @@ +# AI Tasks Client Implementation Summary + +## Overview + +This document describes the client-side implementation of AI-powered tasks for the Progress Planner WordPress plugin. The implementation enables the plugin to fetch AI tasks from the SaaS server and execute them with real-time AI analysis. + +## Architecture + +### Components Created + +1. **API Client** (`/code/classes/class-ai-tasks.php`) + - Handles communication with the SaaS server + - Fetches AI tasks from `/wp-json/progress-planner-saas/v1/suggested-todo` + - Executes AI tasks via `/wp-json/progress-planner-saas/v1/execute-ai-task` + - Implements response caching to avoid repeated API calls + +2. **Task Provider** (`/code/classes/suggested-tasks/providers/class-ai-tasks-from-server.php`) + - Main provider class that manages AI tasks + - Fetches tasks from the server on initialization + - Injects AI tasks into the task system + - Handles AJAX requests for task execution + - Renders the popover UI for AI analysis results + +3. **Base AI Task Class** (`/code/classes/suggested-tasks/providers/class-ai-task.php`) + - Abstract base class for AI-powered tasks + - Can be extended for custom AI task implementations + - Provides common functionality for AI task handling + +4. **JavaScript Handler** (`/code/assets/js/recommendations/ai-task.js`) + - Manages client-side AI task execution + - Handles loading states, results display, and error handling + - Formats AI responses with basic HTML formatting + - Integrates with the existing task management system + +5. **Styles** (`/code/assets/css/ai-task.css`) + - Visual styling for AI task UI components + - Responsive design for mobile and desktop + - Loading spinner animations + - Result and error state styling + +## Integration Points + +### 1. Tasks Manager Registration + +The AI task provider is registered in `/code/classes/suggested-tasks/class-tasks-manager.php`: + +```php +use Progress_Planner\Suggested_Tasks\Providers\AI_Tasks_From_Server; + +// In constructor: +new AI_Tasks_From_Server(), +``` + +### 2. Base Class Method + +The AI_Tasks class is accessible via the main plugin instance: + +```php +\progress_planner()->get_ai_tasks() +``` + +This is defined in `/code/classes/class-base.php` with the `@method` annotation. + +### 3. REST API Metadata + +AI task metadata fields are registered in `/code/classes/class-suggested-tasks.php`: + +- `is_ai_task` (boolean) - Identifies AI-powered tasks +- `ai_task_server_id` (number) - Server-side task ID +- `ai_prompt_template` (string) - Template for AI prompts +- `ai_provider` (string) - AI provider (e.g., "chatgpt") +- `ai_max_tokens` (number) - Token limit for AI responses +- `branding` (string) - Branding identifier for task filtering + +## User Flow + +1. **Task Discovery** + - Plugin fetches AI tasks from SaaS server on initialization + - Tasks are injected into the local task database + - AI tasks appear in the task list with an "Analyze" button + +2. **Task Execution** + - User clicks "Analyze" button + - Popover opens with task description + - User clicks "Analyze" in popover to execute + - Loading indicator shows during AI processing (10-30 seconds) + +3. **Result Display** + - AI response is formatted and displayed in the popover + - Response is cached to avoid repeated API calls + - User can retry on error + +4. **Error Handling** + - Network errors display friendly error messages + - Retry button appears on errors + - Timeout handling for long-running requests + +## API Communication + +### Fetch AI Tasks + +**Request:** +``` +GET /wp-json/progress-planner-saas/v1/suggested-todo +Parameters: + - site: Current site URL + - license_key: Plugin license key + - branding: (optional) Branding filter +``` + +**Response:** +```json +[ + { + "task_id": 123, + "title": "Analyze Your Homepage", + "is_ai_task": true, + "ai_prompt_template": "Analyze the homepage of ...", + "ai_provider": "chatgpt", + "ai_max_tokens": 500, + "branding": "" + } +] +``` + +### Execute AI Task + +**Request:** +``` +POST /wp-json/progress-planner-saas/v1/execute-ai-task +Body: + - task_id: Server task ID + - site_url: Site URL to analyze + - license_key: Plugin license key +``` + +**Response (Success):** +```json +{ + "success": true, + "task_id": 123, + "ai_response": "This site appears to be...", + "token_usage": { + "prompt_tokens": 150, + "completion_tokens": 85, + "total_tokens": 235 + } +} +``` + +**Response (Error):** +```json +{ + "code": "ai_execution_failed", + "message": "Error description", + "data": {"status": 500} +} +``` + +## Caching Strategy + +The implementation uses WordPress transients for caching: + +- **Cache Key:** `prpl_ai_response_{task_id}` +- **Cache Duration:** 1 week (WEEK_IN_SECONDS) +- **Cache Purpose:** Avoid repeated API calls for the same task +- **Cache Clearing:** Can be manually cleared per task + +## Security Considerations + +1. **Capability Checks** + - Only users with `manage_options` capability can execute AI tasks + - Same capability required for viewing and managing tasks + +2. **Nonce Verification** + - All AJAX requests use WordPress nonce verification + - Nonce is generated with `progress_planner` action + +3. **Data Sanitization** + - All user input is sanitized using WordPress functions + - Task IDs validated before API calls + +4. **Rate Limiting** + - Server-side rate limiting handled by SaaS server + - Client-side caching reduces unnecessary requests + +## File Structure + +``` +/code/ +├── classes/ +│ ├── class-ai-tasks.php # API client +│ ├── class-base.php # Updated with AI_Tasks method +│ ├── class-suggested-tasks.php # Updated with AI metadata +│ └── suggested-tasks/ +│ ├── class-tasks-manager.php # Updated with AI provider +│ └── providers/ +│ ├── class-ai-task.php # Base AI task class +│ └── class-ai-tasks-from-server.php # AI task provider +├── assets/ +│ ├── js/ +│ │ └── recommendations/ +│ │ └── ai-task.js # JavaScript handler +│ └── css/ +│ └── ai-task.css # Styles +└── AI_TASKS_IMPLEMENTATION.md # This file +``` + +## Testing Checklist + +Before deploying, verify: + +- [ ] AI tasks are fetched from the SaaS server on plugin load +- [ ] Tasks appear in the task list with proper styling +- [ ] "Analyze" button opens the popover correctly +- [ ] Loading state displays during AI processing +- [ ] AI responses are formatted and displayed properly +- [ ] Cached responses show "(Cached result)" indicator +- [ ] Error messages display correctly on failures +- [ ] Retry functionality works after errors +- [ ] Only users with `manage_options` can execute tasks +- [ ] Nonce verification prevents CSRF attacks +- [ ] CSS loads properly on Progress Planner and Dashboard pages +- [ ] JavaScript has no console errors +- [ ] Mobile responsive design works correctly + +## Future Enhancements + +Potential improvements for future versions: + +1. **Task Completion Tracking** + - Mark AI tasks as complete after viewing response + - Award points for completing AI tasks + +2. **Response History** + - Store multiple AI responses per task + - Allow users to view previous analyses + +3. **Custom AI Prompts** + - Allow users to modify AI prompts + - Provide prompt templates for common use cases + +4. **Branding Integration** + - Filter tasks by site branding + - Show brand-specific AI recommendations + +5. **Analytics Integration** + - Track AI task usage and completion rates + - Measure effectiveness of AI recommendations + +6. **Export Functionality** + - Export AI responses to PDF or text + - Share responses via email + +## Support and Troubleshooting + +### Common Issues + +1. **Tasks Not Appearing** + - Verify license key is set correctly + - Check server-side task configuration + - Ensure SaaS server is accessible + +2. **Execution Failures** + - Check network connectivity + - Verify API credentials + - Review server-side error logs + +3. **Styling Issues** + - Clear browser cache + - Verify CSS file is enqueued + - Check for theme conflicts + +### Debug Mode + +Enable WordPress debug mode to see detailed error messages: + +```php +define('WP_DEBUG', true); +define('WP_DEBUG_LOG', true); +``` + +## Credits + +Implementation by: Sculptor AI +Based on: Progress Planner architecture and SaaS server integration +Date: 2025-10-28 diff --git a/AI_TASKS_QUICK_START.md b/AI_TASKS_QUICK_START.md new file mode 100644 index 000000000..02af0db27 --- /dev/null +++ b/AI_TASKS_QUICK_START.md @@ -0,0 +1,247 @@ +# AI Tasks - Quick Start Guide + +## What Was Implemented + +The Progress Planner client plugin now supports AI-powered tasks from the SaaS server. Users can execute AI analysis tasks that provide intelligent insights about their website. + +## Files Created + +1. **PHP Classes:** + - `/code/classes/class-ai-tasks.php` - API client for SaaS server communication + - `/code/classes/suggested-tasks/providers/class-ai-task.php` - Base class for AI tasks + - `/code/classes/suggested-tasks/providers/class-ai-tasks-from-server.php` - Main AI task provider + +2. **JavaScript:** + - `/code/assets/js/recommendations/ai-task.js` - Client-side AI task execution handler + +3. **CSS:** + - `/code/assets/css/ai-task.css` - Styles for AI task UI components + +4. **Documentation:** + - `/code/AI_TASKS_IMPLEMENTATION.md` - Detailed implementation documentation + - `/code/AI_TASKS_QUICK_START.md` - This quick start guide + +## Files Modified + +1. **Task Manager Registration:** + - `/code/classes/suggested-tasks/class-tasks-manager.php` + - Added AI_Tasks_From_Server provider to the task providers list + +2. **Base Class:** + - `/code/classes/class-base.php` + - Added `@method` annotation for `get_ai_tasks()` + +3. **REST API Metadata:** + - `/code/classes/class-suggested-tasks.php` + - Registered AI task metadata fields for REST API + +## How It Works + +### 1. Task Discovery +When the plugin loads, the AI_Tasks_From_Server provider: +- Fetches AI tasks from the SaaS server endpoint +- Injects them into the local task database +- Tasks appear in the Progress Planner dashboard + +### 2. Task Execution +When a user executes an AI task: +- Clicks "Analyze" button on the task +- Popover opens with task details +- JavaScript sends AJAX request to execute the task +- Server makes API call to SaaS server +- AI response is displayed to the user + +### 3. Caching +- AI responses are cached for 1 week +- Subsequent executions return cached results +- Reduces API calls and costs + +## Server Requirements + +The SaaS server must provide these endpoints: + +1. **GET** `/wp-json/progress-planner-saas/v1/suggested-todo` + - Returns list of AI tasks + - Accepts `site`, `license_key`, and optional `branding` parameters + +2. **POST** `/wp-json/progress-planner-saas/v1/execute-ai-task` + - Executes an AI task + - Requires `task_id`, `site_url`, and `license_key` + - Returns AI-generated response + +## Testing the Implementation + +### Basic Test Flow + +1. **Enable Debug Mode** (optional): + ```php + define('WP_DEBUG', true); + define('WP_DEBUG_LOG', true); + ``` + +2. **Verify License Key**: + - Ensure the site has a valid Progress Planner license key + - Check: Settings > Progress Planner > License + +3. **Check Task Appearance**: + - Navigate to Progress Planner dashboard + - AI tasks should appear with "Analyze" button + - Look for tasks with purple accent color + +4. **Test Task Execution**: + - Click "Analyze" on an AI task + - Popover should open + - Click "Analyze" in popover + - Loading spinner should appear (10-30 seconds) + - AI response should display + +5. **Test Caching**: + - Execute the same task again + - Should return instantly + - Look for "(Cached result)" indicator + +6. **Test Error Handling**: + - Temporarily disconnect from internet + - Try to execute a task + - Should show error message with retry button + +### Debug Checklist + +If tasks don't appear: +- [ ] Check if license key is set +- [ ] Verify SaaS server is accessible +- [ ] Check browser console for JavaScript errors +- [ ] Review WordPress debug.log for PHP errors +- [ ] Ensure user has `manage_options` capability + +If execution fails: +- [ ] Check network tab in browser dev tools +- [ ] Verify AJAX endpoint returns 200 status +- [ ] Check nonce is being sent correctly +- [ ] Review server-side API response +- [ ] Ensure AI task exists on server + +## Key Features + +### 1. Automatic Task Discovery +- Tasks are fetched from SaaS server automatically +- No manual configuration needed +- Tasks sync on plugin initialization + +### 2. Real-time AI Analysis +- Executes AI prompts in real-time +- Analyzes user's actual website +- Provides actionable insights + +### 3. Smart Caching +- Caches responses to avoid redundant API calls +- Configurable cache duration +- Per-task cache management + +### 4. Error Handling +- Graceful degradation on errors +- User-friendly error messages +- Retry functionality + +### 5. Security +- Capability checks (manage_options required) +- Nonce verification on all AJAX calls +- Input sanitization +- No sensitive data exposed + +## Architecture Highlights + +### Plugin Integration +The implementation follows the existing Progress Planner architecture: +- Uses the Task Provider system +- Integrates with the existing task UI +- Follows WordPress coding standards +- Uses the plugin's enqueue system + +### API Communication +- Uses WordPress HTTP API (`wp_remote_get`, `wp_remote_post`) +- Implements proper error handling +- Uses WordPress transients for caching +- Follows REST API conventions + +### Frontend +- Vanilla JavaScript (no external dependencies) +- Uses existing Progress Planner utilities +- Responsive design +- Progressive enhancement + +## Customization + +### Adjusting Cache Duration + +In `/code/classes/class-ai-tasks.php`, modify: + +```php +public function cache_response( $task_id, $response, $expiry = WEEK_IN_SECONDS ) { + // Change WEEK_IN_SECONDS to desired duration +} +``` + +### Changing Task Priority + +In `/code/classes/suggested-tasks/providers/class-ai-tasks-from-server.php`, modify: + +```php +protected $priority = 30; // Lower = higher priority +``` + +### Adding Custom Styling + +Edit `/code/assets/css/ai-task.css` to customize: +- Colors +- Spacing +- Typography +- Animations + +### Customizing AI Response Format + +Edit `/code/assets/js/recommendations/ai-task.js`, function `formatAIResponse()` to change how responses are displayed. + +## Next Steps + +1. **Test with Real Data** + - Create AI tasks on the SaaS server + - Test with various prompts and scenarios + - Verify responses are helpful and accurate + +2. **Monitor Performance** + - Track API response times + - Monitor cache hit rates + - Check for any errors in production + +3. **Gather User Feedback** + - How useful are the AI insights? + - Are the responses clear and actionable? + - Do users understand how to use the feature? + +4. **Iterate and Improve** + - Refine AI prompts based on feedback + - Add more task types + - Enhance the UI/UX + - Consider adding analytics + +## Support + +For issues or questions: +1. Check `/code/AI_TASKS_IMPLEMENTATION.md` for detailed docs +2. Review WordPress debug.log for errors +3. Check browser console for JavaScript errors +4. Verify SaaS server is responding correctly + +## Version Information + +- **Implementation Date**: 2025-10-28 +- **Client Plugin**: Progress Planner +- **Server Integration**: Progress Planner SaaS +- **Dependencies**: WordPress 5.0+, Progress Planner license + +--- + +**Implementation Status**: ✅ Complete + +All core functionality has been implemented and is ready for testing. diff --git a/assets/css/ai-task.css b/assets/css/ai-task.css new file mode 100644 index 000000000..37992ee6f --- /dev/null +++ b/assets/css/ai-task.css @@ -0,0 +1,126 @@ +/** + * AI Task Styles + * + * Styles for AI-powered task execution and display. + */ + +/* AI Task Popover */ +.prpl-popover-ai-task { + max-width: 600px; +} + +.prpl-ai-task-content { + min-height: 100px; +} + +/* Loading State */ +.prpl-ai-task-loading { + text-align: center; + padding: 30px 20px; +} + +.prpl-ai-task-loading .prpl-spinner { + margin: 0 auto 20px; + width: 40px; + height: 40px; + border: 4px solid #f3f4f6; + border-top-color: #534786; + border-radius: 50%; + animation: prpl-spin 1s linear infinite; +} + +@keyframes prpl-spin { + to { + transform: rotate(360deg); + } +} + +.prpl-ai-task-loading p { + color: #666; + font-size: 14px; + margin: 0; +} + +/* Result State */ +.prpl-ai-task-result { + padding: 20px; + background: #f9fafb; + border-radius: 6px; + margin-bottom: 20px; +} + +.prpl-ai-task-response { + line-height: 1.6; + color: #1f2937; +} + +.prpl-ai-task-response p { + margin: 0 0 15px 0; +} + +.prpl-ai-task-response p:last-child { + margin-bottom: 0; +} + +.prpl-ai-cached-indicator { + font-style: italic; + border-top: 1px solid #e5e7eb; + padding-top: 10px; + margin-top: 15px !important; +} + +/* Error State */ +.prpl-ai-task-error { + padding: 15px; + background: #fef2f2; + border: 1px solid #fecaca; + border-radius: 6px; + margin-bottom: 20px; +} + +.prpl-ai-task-error .prpl-error-message { + color: #dc2626; + margin: 0; + font-size: 14px; +} + +/* Buttons */ +.prpl-ai-task-trigger { + background: #534786 !important; + color: white !important; + border: none; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background 0.2s; +} + +.prpl-ai-task-trigger:hover { + background: #453670 !important; +} + +.prpl-ai-task-execute, +.prpl-ai-task-retry { + min-width: 120px; +} + +/* Task List Integration */ +.prpl-suggested-task[data-provider-id="ai-tasks"] { + border-left: 3px solid #8b5cf6; +} + +.prpl-suggested-task[data-provider-id="ai-tasks"] .prpl-task-title { + color: #7c3aed; +} + +/* Responsive */ +@media (max-width: 768px) { + .prpl-popover-ai-task { + max-width: 90vw; + } + + .prpl-ai-task-result { + padding: 15px; + } +} diff --git a/assets/js/recommendations/ai-task.js b/assets/js/recommendations/ai-task.js new file mode 100644 index 000000000..03bb9811a --- /dev/null +++ b/assets/js/recommendations/ai-task.js @@ -0,0 +1,212 @@ +/* global progressPlannerAjaxRequest, progressPlanner */ + +/** + * AI Task Handler + * + * Handles execution of AI-powered tasks from the SaaS server. + * + * Dependencies: progress-planner/suggested-task, progress-planner/ajax-request + */ + +( () => { + /** + * Handle AI task execution. + */ + const handleAITaskExecution = () => { + const popover = document.getElementById( 'prpl-popover-ai-task' ); + if ( ! popover ) { + return; + } + + const executeButton = popover.querySelector( '.prpl-ai-task-execute' ); + const retryButton = popover.querySelector( '.prpl-ai-task-retry' ); + const loadingEl = popover.querySelector( '.prpl-ai-task-loading' ); + const resultEl = popover.querySelector( '.prpl-ai-task-result' ); + const responseEl = popover.querySelector( '.prpl-ai-task-response' ); + const errorEl = popover.querySelector( '.prpl-ai-task-error' ); + const errorMessageEl = popover.querySelector( '.prpl-error-message' ); + + let currentTaskId = null; + + /** + * Show loading state. + */ + const showLoading = () => { + executeButton.style.display = 'none'; + retryButton.style.display = 'none'; + loadingEl.style.display = 'block'; + resultEl.style.display = 'none'; + errorEl.style.display = 'none'; + }; + + /** + * Show result state. + * + * @param {string} response - The AI response text. + * @param {boolean} cached - Whether the response was cached. + */ + const showResult = ( response, cached = false ) => { + loadingEl.style.display = 'none'; + executeButton.style.display = 'none'; + retryButton.style.display = 'none'; + errorEl.style.display = 'none'; + resultEl.style.display = 'block'; + + // Format the response with markdown-like formatting. + const formattedResponse = formatAIResponse( response ); + responseEl.innerHTML = formattedResponse; + + // Add cached indicator if applicable. + if ( cached ) { + const cachedIndicator = document.createElement( 'p' ); + cachedIndicator.className = 'prpl-ai-cached-indicator'; + cachedIndicator.style.fontSize = '0.9em'; + cachedIndicator.style.color = '#666'; + cachedIndicator.style.marginTop = '10px'; + cachedIndicator.textContent = '(Cached result)'; + responseEl.appendChild( cachedIndicator ); + } + }; + + /** + * Show error state. + * + * @param {string} message - The error message. + */ + const showError = ( message ) => { + loadingEl.style.display = 'none'; + executeButton.style.display = 'none'; + retryButton.style.display = 'inline-block'; + resultEl.style.display = 'none'; + errorEl.style.display = 'block'; + errorMessageEl.textContent = message; + }; + + /** + * Format AI response with basic HTML formatting. + * + * @param {string} text - The raw AI response text. + * @return {string} Formatted HTML string. + */ + const formatAIResponse = ( text ) => { + if ( ! text ) { + return ''; + } + + // Convert line breaks to paragraphs. + const paragraphs = text.split( /\n\n+/ ).map( ( p ) => { + const trimmed = p.trim(); + if ( ! trimmed ) { + return ''; + } + // Replace single line breaks with
. + const formatted = trimmed.replace( /\n/g, '
' ); + return `

${ formatted }

`; + } ); + + return paragraphs.join( '' ); + }; + + /** + * Execute the AI task. + * + * @param {number} taskId - The server task ID. + */ + const executeTask = ( taskId ) => { + if ( ! taskId ) { + showError( 'Invalid task ID.' ); + return; + } + + currentTaskId = taskId; + showLoading(); + + // Make AJAX request to execute the AI task. + progressPlannerAjaxRequest( { + action: 'prpl_execute_ai_task', + task_id: taskId, + nonce: progressPlanner.nonce, + } ) + .then( ( response ) => { + if ( ! response.success ) { + const errorMessage = + response.data?.message || + 'Failed to execute AI task. Please try again.'; + showError( errorMessage ); + return; + } + + const data = response.data; + const aiResponse = data.ai_response || ''; + const cached = data.cached || false; + + showResult( aiResponse, cached ); + } ) + .catch( ( error ) => { + console.error( 'AI task execution error:', error ); + showError( + 'An error occurred while analyzing your site. Please try again.' + ); + } ); + }; + + /** + * Set up event listeners for trigger buttons in task list. + */ + const setupTriggerButtons = () => { + const triggerButtons = document.querySelectorAll( + '.prpl-ai-task-trigger' + ); + + triggerButtons.forEach( ( button ) => { + button.addEventListener( 'click', () => { + const taskId = button.dataset.taskId; + if ( taskId ) { + // Store the task ID for execution. + currentTaskId = parseInt( taskId ); + + // Reset the popover state. + loadingEl.style.display = 'none'; + resultEl.style.display = 'none'; + errorEl.style.display = 'none'; + executeButton.style.display = 'inline-block'; + retryButton.style.display = 'none'; + } + } ); + } ); + }; + + // Execute button click handler. + if ( executeButton ) { + executeButton.addEventListener( 'click', () => { + if ( currentTaskId ) { + executeTask( currentTaskId ); + } + } ); + } + + // Retry button click handler. + if ( retryButton ) { + retryButton.addEventListener( 'click', () => { + if ( currentTaskId ) { + executeTask( currentTaskId ); + } + } ); + } + + // Set up trigger buttons initially. + setupTriggerButtons(); + + // Re-setup trigger buttons when new tasks are injected. + document.addEventListener( 'prpl/suggestedTask/injectItem', () => { + setTimeout( setupTriggerButtons, 100 ); + } ); + }; + + // Initialize when DOM is ready. + if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', handleAITaskExecution ); + } else { + handleAITaskExecution(); + } +} )(); diff --git a/classes/class-ai-tasks.php b/classes/class-ai-tasks.php new file mode 100644 index 000000000..c63650a7c --- /dev/null +++ b/classes/class-ai-tasks.php @@ -0,0 +1,156 @@ +get_remote_server_root_url() . '/wp-json/progress-planner-saas/v1/suggested-todo'; + + $query_args = [ + 'site' => \get_site_url(), + 'license_key' => \get_option( 'progress_planner_license_key' ), + ]; + + // Add optional branding parameter if provided. + if ( ! empty( $args['branding'] ) ) { + $query_args['branding'] = $args['branding']; + } + + $url = \add_query_arg( $query_args, $url ); + + $cache_key = \md5( $url ); + + $cached = \progress_planner()->get_utils__cache()->get( $cache_key ); + if ( \is_array( $cached ) ) { + return $cached; + } + + $response = \wp_remote_get( $url ); + + if ( \is_wp_error( $response ) ) { + \error_log( 'Progress Planner AI Tasks: Failed to fetch tasks from server - ' . $response->get_error_message() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + \progress_planner()->get_utils__cache()->set( $cache_key, [], 5 * MINUTE_IN_SECONDS ); + return []; + } + + $response_code = (int) \wp_remote_retrieve_response_code( $response ); + if ( 200 !== $response_code ) { + \error_log( 'Progress Planner AI Tasks: Server returned error code ' . $response_code . ' when fetching tasks' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + \progress_planner()->get_utils__cache()->set( $cache_key, [], 5 * MINUTE_IN_SECONDS ); + return []; + } + + $json = \json_decode( \wp_remote_retrieve_body( $response ), true ); + if ( ! \is_array( $json ) ) { + \progress_planner()->get_utils__cache()->set( $cache_key, [], 5 * MINUTE_IN_SECONDS ); + return []; + } + + \progress_planner()->get_utils__cache()->set( $cache_key, $json, HOUR_IN_SECONDS ); + + return $json; + } + + /** + * Execute an AI task. + * + * @param int $task_id The task ID to execute. + * @param string $site_url The site URL to analyze. + * @param string $license_key The license key. + * @return array|WP_Error Response array on success, WP_Error on failure. + */ + public function execute_ai_task( $task_id, $site_url, $license_key ) { + $url = \progress_planner()->get_remote_server_root_url() . '/wp-json/progress-planner-saas/v1/execute-ai-task'; + + $response = \wp_remote_post( + $url, + [ + 'body' => [ + 'task_id' => $task_id, + 'site_url' => $site_url, + 'license_key' => $license_key, + ], + 'timeout' => 45, // AI tasks may take longer. + ] + ); + + if ( \is_wp_error( $response ) ) { + return $response; + } + + $response_code = \wp_remote_retrieve_response_code( $response ); + $body = \wp_remote_retrieve_body( $response ); + $json = \json_decode( $body, true ); + + if ( 200 !== $response_code ) { + $error_message = isset( $json['message'] ) ? $json['message'] : 'Unknown error occurred'; + return new \WP_Error( 'ai_execution_failed', $error_message, [ 'status' => $response_code ] ); + } + + if ( ! \is_array( $json ) ) { + return new \WP_Error( 'invalid_response', 'Invalid response from server', [ 'status' => 500 ] ); + } + + return $json; + } + + /** + * Get a cached AI response for a task. + * + * @param int $task_id The task ID. + * @return string|false The cached response or false if not cached. + */ + public function get_cached_response( $task_id ) { + $cache_key = 'prpl_ai_response_' . $task_id; + return \get_transient( $cache_key ); + } + + /** + * Cache an AI response for a task. + * + * @param int $task_id The task ID. + * @param string $response The AI response. + * @param int $expiry Optional. Cache expiry in seconds. Default 1 week. + * @return bool True on success, false on failure. + */ + public function cache_response( $task_id, $response, $expiry = WEEK_IN_SECONDS ) { + $cache_key = 'prpl_ai_response_' . $task_id; + return \set_transient( $cache_key, $response, $expiry ); + } + + /** + * Clear the cached AI response for a task. + * + * @param int $task_id The task ID. + * @return bool True on success, false on failure. + */ + public function clear_cached_response( $task_id ) { + $cache_key = 'prpl_ai_response_' . $task_id; + return \delete_transient( $cache_key ); + } +} diff --git a/classes/class-base.php b/classes/class-base.php index 2aa14bbd4..49e60ecd4 100644 --- a/classes/class-base.php +++ b/classes/class-base.php @@ -55,6 +55,7 @@ * @method \Progress_Planner\Admin\Widgets\Challenge get_admin__widgets__challenge() * @method \Progress_Planner\Admin\Widgets\Activity_Scores get_admin__widgets__activity_scores() * @method \Progress_Planner\Utils\Date get_utils__date() + * @method \Progress_Planner\AI_Tasks get_ai_tasks() */ class Base { diff --git a/classes/class-suggested-tasks.php b/classes/class-suggested-tasks.php index 1e0dfeeeb..64694027b 100644 --- a/classes/class-suggested-tasks.php +++ b/classes/class-suggested-tasks.php @@ -375,17 +375,48 @@ public function register_post_type() { ); $rest_meta_fields = [ - 'prpl_url' => [ + 'prpl_url' => [ 'type' => 'string', 'single' => true, 'show_in_rest' => true, ], - 'menu_order' => [ + 'menu_order' => [ 'type' => 'number', 'single' => true, 'show_in_rest' => true, 'default' => 0, ], + 'is_ai_task' => [ + 'type' => 'boolean', + 'single' => true, + 'show_in_rest' => true, + 'default' => false, + ], + 'ai_task_server_id' => [ + 'type' => 'number', + 'single' => true, + 'show_in_rest' => true, + ], + 'ai_prompt_template' => [ + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + ], + 'ai_provider' => [ + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + ], + 'ai_max_tokens' => [ + 'type' => 'number', + 'single' => true, + 'show_in_rest' => true, + ], + 'branding' => [ + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + ], ]; foreach ( $rest_meta_fields as $key => $field ) { diff --git a/classes/suggested-tasks/class-tasks-manager.php b/classes/suggested-tasks/class-tasks-manager.php index d20164df9..44c72cecd 100644 --- a/classes/suggested-tasks/class-tasks-manager.php +++ b/classes/suggested-tasks/class-tasks-manager.php @@ -39,6 +39,7 @@ use Progress_Planner\Suggested_Tasks\Providers\Set_Date_Format; use Progress_Planner\Suggested_Tasks\Providers\SEO_Plugin; use Progress_Planner\Suggested_Tasks\Providers\Improve_Pdf_Handling; +use Progress_Planner\Suggested_Tasks\Providers\AI_Tasks_From_Server; /** * Tasks_Manager class. @@ -87,6 +88,7 @@ public function __construct() { new Set_Date_Format(), new SEO_Plugin(), new Improve_Pdf_Handling(), + new AI_Tasks_From_Server(), ]; // Add the plugin integration. diff --git a/classes/suggested-tasks/providers/class-ai-task.php b/classes/suggested-tasks/providers/class-ai-task.php new file mode 100644 index 000000000..fd19a3ef6 --- /dev/null +++ b/classes/suggested-tasks/providers/class-ai-task.php @@ -0,0 +1,230 @@ +ai_task_data = $ai_task_data; + } + + /** + * Get the task title. + * + * @return string + */ + protected function get_title() { + return $this->ai_task_data['title'] ?? ''; + } + + /** + * Get the task description. + * + * @return string + */ + protected function get_description() { + return $this->ai_task_data['description'] ?? 'Click "Analyze" to get AI-powered insights.'; + } + + /** + * Check if this should be added as a task. + * AI tasks from server should always be added. + * + * @return bool + */ + public function should_add_task() { + // AI tasks are managed by the SaaS server, so we always show them if they exist. + return ! empty( $this->ai_task_data ); + } + + /** + * Get the task ID. + * + * @param array $task_data Optional data to include in the task ID. + * @return string + */ + public function get_task_id( $task_data = [] ) { + $server_task_id = $this->ai_task_data['task_id'] ?? 0; + return 'ai-task-' . $server_task_id; + } + + /** + * Get the task details. + * + * @param array $task_data The task data. + * + * @return array + */ + public function get_task_details( $task_data = [] ) { + $details = parent::get_task_details( $task_data ); + + // Add AI-specific metadata. + $details['is_ai_task'] = true; + $details['ai_task_id'] = $this->ai_task_data['task_id'] ?? 0; + $details['ai_prompt_template'] = $this->ai_task_data['ai_prompt_template'] ?? ''; + $details['ai_provider'] = $this->ai_task_data['ai_provider'] ?? 'chatgpt'; + $details['ai_max_tokens'] = $this->ai_task_data['ai_max_tokens'] ?? 500; + $details['branding'] = $this->ai_task_data['branding'] ?? ''; + + return $details; + } + + /** + * Handle AI task execution via AJAX. + * + * @return void + */ + public function handle_ai_task_execution() { + // Check if the user has the necessary capabilities. + if ( ! \current_user_can( 'manage_options' ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'You do not have permission to execute AI tasks.', 'progress-planner' ) ] ); + } + + // Check the nonce. + if ( ! \check_ajax_referer( 'progress_planner', 'nonce', false ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid nonce.', 'progress-planner' ) ] ); + } + + if ( ! isset( $_POST['task_id'] ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Missing task ID.', 'progress-planner' ) ] ); + } + + $task_id = (int) \sanitize_text_field( \wp_unslash( $_POST['task_id'] ) ); + + // Check if we have a cached response. + $cached_response = \progress_planner()->get_ai_tasks()->get_cached_response( $task_id ); + if ( false !== $cached_response ) { + \wp_send_json_success( + [ + 'success' => true, + 'task_id' => $task_id, + 'ai_response' => $cached_response, + 'cached' => true, + ] + ); + } + + // Execute the AI task via the SaaS server. + $site_url = \get_site_url(); + $license_key = \get_option( 'progress_planner_license_key' ); + + $result = \progress_planner()->get_ai_tasks()->execute_ai_task( $task_id, $site_url, $license_key ); + + if ( \is_wp_error( $result ) ) { + \wp_send_json_error( + [ + 'message' => $result->get_error_message(), + 'code' => $result->get_error_code(), + ] + ); + } + + // Cache the response. + if ( isset( $result['ai_response'] ) ) { + \progress_planner()->get_ai_tasks()->cache_response( $task_id, $result['ai_response'] ); + } + + \wp_send_json_success( $result ); + } + + /** + * Get the list of allowed options that can be updated via interactive tasks. + * + * @return array List of allowed option names. + */ + protected function get_allowed_interactive_options() { + // AI tasks don't update options via the standard interactive form. + return []; + } + + /** + * Print the popover form contents. + * + * @return void + */ + public function print_popover_form_contents() { + $task_id = $this->ai_task_data['task_id'] ?? 0; + ?> +
+ + + +
+
+ + +
+ 'recommendations/ai-task.js', + 'dependencies' => [ 'progress-planner/suggested-task' ], + ]; + } + + /** + * Add task actions for AI tasks. + * + * @param array $data The task data. + * @param array $actions The existing actions. + * + * @return array + */ + public function add_task_actions( $data = [], $actions = [] ) { + // Add the "Analyze" button that opens the popover. + $actions[] = [ + 'priority' => 10, + 'html' => '', + ]; + + return $actions; + } +} diff --git a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php new file mode 100644 index 000000000..aacab7219 --- /dev/null +++ b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php @@ -0,0 +1,302 @@ +ai_tasks = \progress_planner()->get_ai_tasks()->get_items(); + + // Add AJAX handler for AI task execution. + \add_action( 'wp_ajax_prpl_execute_ai_task', [ $this, 'handle_ai_task_execution' ] ); + + // Add popover for AI tasks. + \add_action( 'progress_planner_admin_page_after_widgets', [ $this, 'add_popover' ] ); + \add_action( 'progress_planner_admin_dashboard_widget_score_after', [ $this, 'add_popover' ] ); + + // Enqueue AI task scripts. + \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); + } + + /** + * Check if we should add AI tasks. + * + * @return bool + */ + public function should_add_task() { + // Return true if we have AI tasks from the server. + return ! empty( $this->ai_tasks ); + } + + /** + * Get tasks to inject. + * Override to inject multiple AI tasks. + * + * @return array + */ + public function get_tasks_to_inject() { + if ( empty( $this->ai_tasks ) ) { + return []; + } + + $tasks_to_inject = []; + + foreach ( $this->ai_tasks as $ai_task ) { + $task_id = 'ai-task-' . $ai_task['task_id']; + + // Skip if already completed or injected. + if ( \progress_planner()->get_suggested_tasks()->was_task_completed( $task_id ) ) { + continue; + } + + if ( \progress_planner()->get_suggested_tasks_db()->get_post( $task_id ) ) { + continue; + } + + // Create task data. + $task_data = [ + 'task_id' => $task_id, + 'provider_id' => $this->get_provider_id(), + 'post_title' => $ai_task['title'] ?? 'AI Task', + 'description' => 'Click "Analyze" to get AI-powered insights for your site.', + 'parent' => 0, + 'priority' => $this->get_priority(), + 'points' => $this->get_points(), + 'date' => \gmdate( 'YW' ), + 'url' => '', + 'url_target' => '_self', + 'link_setting' => [], + 'dismissable' => $this->is_dismissable(), + 'external_link_url' => '', + 'popover_id' => 'prpl-popover-' . static::POPOVER_ID, + 'is_ai_task' => true, + 'ai_task_server_id' => $ai_task['task_id'], + 'ai_prompt_template' => $ai_task['ai_prompt_template'] ?? '', + 'ai_provider' => $ai_task['ai_provider'] ?? 'chatgpt', + 'ai_max_tokens' => $ai_task['ai_max_tokens'] ?? 500, + 'branding' => $ai_task['branding'] ?? '', + ]; + + $tasks_to_inject[] = \progress_planner()->get_suggested_tasks_db()->add( $task_data ); + } + + return $tasks_to_inject; + } + + /** + * Handle AI task execution via AJAX. + * + * @return void + */ + public function handle_ai_task_execution() { + // Check if the user has the necessary capabilities. + if ( ! \current_user_can( 'manage_options' ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'You do not have permission to execute AI tasks.', 'progress-planner' ) ] ); + } + + // Check the nonce. + if ( ! \check_ajax_referer( 'progress_planner', 'nonce', false ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid nonce.', 'progress-planner' ) ] ); + } + + if ( ! isset( $_POST['task_id'] ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Missing task ID.', 'progress-planner' ) ] ); + } + + $task_id = (int) \sanitize_text_field( \wp_unslash( $_POST['task_id'] ) ); + + // Check if we have a cached response. + $cached_response = \progress_planner()->get_ai_tasks()->get_cached_response( $task_id ); + if ( false !== $cached_response ) { + \wp_send_json_success( + [ + 'success' => true, + 'task_id' => $task_id, + 'ai_response' => $cached_response, + 'cached' => true, + ] + ); + } + + // Execute the AI task via the SaaS server. + $site_url = \get_site_url(); + $license_key = \get_option( 'progress_planner_license_key' ); + + $result = \progress_planner()->get_ai_tasks()->execute_ai_task( $task_id, $site_url, $license_key ); + + if ( \is_wp_error( $result ) ) { + \wp_send_json_error( + [ + 'message' => $result->get_error_message(), + 'code' => $result->get_error_code(), + ] + ); + } + + // Cache the response. + if ( isset( $result['ai_response'] ) ) { + \progress_planner()->get_ai_tasks()->cache_response( $task_id, $result['ai_response'] ); + } + + \wp_send_json_success( $result ); + } + + /** + * Add the popover for AI tasks. + * + * @return void + */ + public function add_popover() { + ?> +
+
+

+ +
+
+
+ + + +
+
+ + +
+
+
+ get_admin__enqueue()->enqueue_script( + 'progress-planner/recommendations/ai-task', + [ + 'file' => 'recommendations/ai-task.js', + 'dependencies' => [ 'progress-planner/suggested-task' ], + ] + ); + + // Enqueue the AI task CSS. + \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/ai-task' ); + } + + /** + * Get task actions for AI tasks. + * + * @param array $data The task data. + * @param array $actions The existing actions. + * + * @return array + */ + public function add_task_actions( $data = [], $actions = [] ) { + // Add the "Analyze" button that opens the popover. + $actions[] = [ + 'priority' => 10, + 'html' => '', + ]; + + return $actions; + } +} From fc9ed829020296a74b3f2ba829c0edfc7327b2f1 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Tue, 28 Oct 2025 16:36:29 +0000 Subject: [PATCH 2/8] more ai changes Co-authored-by: Sculptor --- assets/css/ai-task.css | 15 --- assets/js/recommendations/ai-task.js | 62 +++++++-- .../js/web-components/prpl-ai-task-popover.js | 120 ++++++++++++++++++ classes/class-ai-tasks.php | 27 ++-- .../providers/class-ai-tasks-from-server.php | 99 ++++++++++----- 5 files changed, 261 insertions(+), 62 deletions(-) create mode 100644 assets/js/web-components/prpl-ai-task-popover.js diff --git a/assets/css/ai-task.css b/assets/css/ai-task.css index 37992ee6f..b690e4aee 100644 --- a/assets/css/ai-task.css +++ b/assets/css/ai-task.css @@ -85,21 +85,6 @@ } /* Buttons */ -.prpl-ai-task-trigger { - background: #534786 !important; - color: white !important; - border: none; - padding: 6px 12px; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - transition: background 0.2s; -} - -.prpl-ai-task-trigger:hover { - background: #453670 !important; -} - .prpl-ai-task-execute, .prpl-ai-task-retry { min-width: 120px; diff --git a/assets/js/recommendations/ai-task.js b/assets/js/recommendations/ai-task.js index 03bb9811a..2b99aec4a 100644 --- a/assets/js/recommendations/ai-task.js +++ b/assets/js/recommendations/ai-task.js @@ -20,6 +20,8 @@ const executeButton = popover.querySelector( '.prpl-ai-task-execute' ); const retryButton = popover.querySelector( '.prpl-ai-task-retry' ); + const completeButton = popover.querySelector( '.prpl-ai-task-complete' ); + const instructionsEl = popover.querySelector( '.prpl-ai-task-instructions' ); const loadingEl = popover.querySelector( '.prpl-ai-task-loading' ); const resultEl = popover.querySelector( '.prpl-ai-task-result' ); const responseEl = popover.querySelector( '.prpl-ai-task-response' ); @@ -34,6 +36,10 @@ const showLoading = () => { executeButton.style.display = 'none'; retryButton.style.display = 'none'; + completeButton.style.display = 'none'; + if ( instructionsEl ) { + instructionsEl.style.display = 'none'; + } loadingEl.style.display = 'block'; resultEl.style.display = 'none'; errorEl.style.display = 'none'; @@ -46,11 +52,15 @@ * @param {boolean} cached - Whether the response was cached. */ const showResult = ( response, cached = false ) => { + if ( instructionsEl ) { + instructionsEl.style.display = 'none'; + } loadingEl.style.display = 'none'; executeButton.style.display = 'none'; retryButton.style.display = 'none'; errorEl.style.display = 'none'; resultEl.style.display = 'block'; + completeButton.style.display = 'inline-block'; // Format the response with markdown-like formatting. const formattedResponse = formatAIResponse( response ); @@ -74,9 +84,13 @@ * @param {string} message - The error message. */ const showError = ( message ) => { + if ( instructionsEl ) { + instructionsEl.style.display = 'none'; + } loadingEl.style.display = 'none'; executeButton.style.display = 'none'; retryButton.style.display = 'inline-block'; + completeButton.style.display = 'none'; resultEl.style.display = 'none'; errorEl.style.display = 'block'; errorMessageEl.textContent = message; @@ -123,9 +137,12 @@ // Make AJAX request to execute the AI task. progressPlannerAjaxRequest( { - action: 'prpl_execute_ai_task', - task_id: taskId, - nonce: progressPlanner.nonce, + url: progressPlanner.ajaxUrl, + data: { + action: 'prpl_execute_ai_task', + task_id: taskId, + nonce: progressPlanner.nonce, + }, } ) .then( ( response ) => { if ( ! response.success ) { @@ -158,19 +175,42 @@ '.prpl-ai-task-trigger' ); + console.log( 'Found AI task trigger buttons:', triggerButtons.length ); + triggerButtons.forEach( ( button ) => { button.addEventListener( 'click', () => { const taskId = button.dataset.taskId; + console.log( 'Trigger button clicked, task ID from data attribute:', taskId ); + + // Get the task element and its slug for completion + const taskElement = button.closest( '.prpl-suggested-task' ); + const taskSlug = taskElement ? taskElement.dataset.taskId : null; + console.log( 'Task slug from element:', taskSlug ); + if ( taskId ) { - // Store the task ID for execution. + // Store the task ID on the execute button + executeButton.dataset.taskId = taskId; + retryButton.dataset.taskId = taskId; currentTaskId = parseInt( taskId ); + console.log( 'Set currentTaskId to:', currentTaskId ); + + // Store the task slug on the web component so completeTask can find it + const webComponent = popover.querySelector( 'prpl-ai-task-popover' ); + if ( webComponent && taskSlug ) { + webComponent.setAttribute( 'current-task-id', taskSlug ); + console.log( 'Set current-task-id on web component:', taskSlug ); + } // Reset the popover state. + if ( instructionsEl ) { + instructionsEl.style.display = 'block'; + } loadingEl.style.display = 'none'; resultEl.style.display = 'none'; errorEl.style.display = 'none'; executeButton.style.display = 'inline-block'; retryButton.style.display = 'none'; + completeButton.style.display = 'none'; } } ); } ); @@ -179,8 +219,13 @@ // Execute button click handler. if ( executeButton ) { executeButton.addEventListener( 'click', () => { - if ( currentTaskId ) { - executeTask( currentTaskId ); + // Get task ID from button's data attribute as fallback + const taskId = currentTaskId || parseInt( executeButton.dataset.taskId ); + console.log( 'Execute button clicked, task ID:', taskId, '(currentTaskId:', currentTaskId, ')' ); + if ( taskId ) { + executeTask( taskId ); + } else { + console.error( 'No task ID set' ); } } ); } @@ -188,8 +233,9 @@ // Retry button click handler. if ( retryButton ) { retryButton.addEventListener( 'click', () => { - if ( currentTaskId ) { - executeTask( currentTaskId ); + const taskId = currentTaskId || parseInt( retryButton.dataset.taskId ); + if ( taskId ) { + executeTask( taskId ); } } ); } diff --git a/assets/js/web-components/prpl-ai-task-popover.js b/assets/js/web-components/prpl-ai-task-popover.js new file mode 100644 index 000000000..540e916bd --- /dev/null +++ b/assets/js/web-components/prpl-ai-task-popover.js @@ -0,0 +1,120 @@ +/* global HTMLElement, prplSuggestedTask, customElements, PrplInteractiveTask */ + +/** + * AI Task Popover Web Component + * + * Extends PrplInteractiveTask to handle AI task completion. + */ +customElements.define( + 'prpl-ai-task-popover', + class extends PrplInteractiveTask { + constructor() { + super(); + this.listenersAttached = false; + } + + /** + * Attach button event listeners. + * Overrides parent to prevent duplicate listeners. + */ + attachDefaultEventListeners() { + // Prevent attaching listeners multiple times + if ( this.listenersAttached ) { + console.log( 'AI Task: Event listeners already attached, skipping' ); + return; + } + + console.log( 'AI Task: Attaching event listeners' ); + + // Add event listeners. + this.querySelectorAll( 'button' ).forEach( ( buttonElement ) => { + buttonElement.addEventListener( 'click', ( e ) => { + const button = e.target.closest( 'button' ); + const action = button?.dataset.action; + if ( action && typeof this[ action ] === 'function' ) { + this[ action ](); + } + } ); + } ); + + this.listenersAttached = true; + } + + /** + * Complete the task. + * Overrides parent to use current-task-id attribute instead of provider-id. + */ + completeTask() { + console.log( '=== AI Task completeTask called ===' ); + console.trace( 'Stack trace' ); + + // Prevent multiple completions + if ( this.isCompleting ) { + console.warn( 'AI Task: Already completing, ignoring duplicate call' ); + return; + } + + this.isCompleting = true; + + // Get the current task ID that was set when the popover opened + const currentTaskId = this.getAttribute( 'current-task-id' ); + + if ( ! currentTaskId ) { + console.error( 'No current-task-id set on AI task popover' ); + this.isCompleting = false; + return; + } + + console.log( 'AI Task: Completing task with ID:', currentTaskId ); + + const tasks = document.querySelectorAll( + '#prpl-suggested-tasks-list .prpl-suggested-task' + ); + + console.log( 'AI Task: Found', tasks.length, 'task elements in list' ); + + let foundMatch = false; + + tasks.forEach( ( taskElement ) => { + console.log( 'AI Task: Checking task element with ID:', taskElement.dataset.taskId ); + if ( taskElement.dataset.taskId === currentTaskId ) { + console.log( 'AI Task: Found matching task element' ); + + if ( foundMatch ) { + console.warn( 'AI Task: Found duplicate matching task element! This should not happen.' ); + return; + } + + foundMatch = true; + + // Close popover. + const popoverId = this.getAttribute( 'popover-id' ); + const popover = document.getElementById( popoverId ); + if ( popover ) { + popover.hidePopover(); + } + + const postId = parseInt( taskElement.dataset.postId ); + + if ( postId ) { + console.log( 'AI Task: Calling maybeComplete with post ID:', postId ); + prplSuggestedTask.maybeComplete( postId ); + + // Reset flag after a delay + setTimeout( () => { + this.isCompleting = false; + }, 1000 ); + } else { + console.error( 'AI Task: No post ID found on task element' ); + this.isCompleting = false; + } + } + } ); + + if ( ! foundMatch ) { + console.error( 'AI Task: No matching task element found' ); + this.isCompleting = false; + } + } + } +); diff --git a/classes/class-ai-tasks.php b/classes/class-ai-tasks.php index c63650a7c..7e03d897a 100644 --- a/classes/class-ai-tasks.php +++ b/classes/class-ai-tasks.php @@ -87,19 +87,25 @@ public function get_items( $args = [] ) { public function execute_ai_task( $task_id, $site_url, $license_key ) { $url = \progress_planner()->get_remote_server_root_url() . '/wp-json/progress-planner-saas/v1/execute-ai-task'; + // Testing. + $site_url = 'https://progressplanner.com'; + + $post_data = [ + 'task_id' => $task_id, + 'site_url' => $site_url, + 'license_key' => $license_key, + ]; + $response = \wp_remote_post( $url, [ - 'body' => [ - 'task_id' => $task_id, - 'site_url' => $site_url, - 'license_key' => $license_key, - ], + 'body' => $post_data, 'timeout' => 45, // AI tasks may take longer. ] ); if ( \is_wp_error( $response ) ) { + \error_log( 'WP_Error response: ' . $response->get_error_message() ); return $response; } @@ -107,6 +113,11 @@ public function execute_ai_task( $task_id, $site_url, $license_key ) { $body = \wp_remote_retrieve_body( $response ); $json = \json_decode( $body, true ); + // Debug logging for response. + \error_log( 'SaaS server response:' ); + \error_log( ' Response code: ' . $response_code ); + \error_log( ' Response body: ' . $body ); + if ( 200 !== $response_code ) { $error_message = isset( $json['message'] ) ? $json['message'] : 'Unknown error occurred'; return new \WP_Error( 'ai_execution_failed', $error_message, [ 'status' => $response_code ] ); @@ -127,7 +138,7 @@ public function execute_ai_task( $task_id, $site_url, $license_key ) { */ public function get_cached_response( $task_id ) { $cache_key = 'prpl_ai_response_' . $task_id; - return \get_transient( $cache_key ); + return progress_planner()->get_utils__cache()->get( $cache_key ); } /** @@ -140,7 +151,7 @@ public function get_cached_response( $task_id ) { */ public function cache_response( $task_id, $response, $expiry = WEEK_IN_SECONDS ) { $cache_key = 'prpl_ai_response_' . $task_id; - return \set_transient( $cache_key, $response, $expiry ); + return progress_planner()->get_utils__cache()->set( $cache_key, $response, $expiry ); } /** @@ -151,6 +162,6 @@ public function cache_response( $task_id, $response, $expiry = WEEK_IN_SECONDS ) */ public function clear_cached_response( $task_id ) { $cache_key = 'prpl_ai_response_' . $task_id; - return \delete_transient( $cache_key ); + return progress_planner()->get_utils__cache()->delete( $cache_key ); } } diff --git a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php index aacab7219..3faacfff7 100644 --- a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php +++ b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php @@ -45,7 +45,7 @@ class AI_Tasks_From_Server extends Tasks { * * @var int */ - protected $points = 3; + protected $points = 1; /** * AI tasks can be dismissed. @@ -190,6 +190,20 @@ public function handle_ai_task_execution() { $site_url = \get_site_url(); $license_key = \get_option( 'progress_planner_license_key' ); + // Debug logging. + \error_log( 'AI Task Execution Debug:' ); + \error_log( ' Task ID: ' . $task_id ); + \error_log( ' Site URL: ' . $site_url ); + \error_log( ' License key exists: ' . ( ! empty( $license_key ) ? 'yes' : 'no' ) ); + \error_log( ' License key length: ' . \strlen( $license_key ) ); + \error_log( ' License key first 10 chars: ' . \substr( $license_key, 0, 10 ) . '...' ); + \error_log( ' License key type: ' . \gettype( $license_key ) ); + + // Validate we have the required data. + if ( empty( $license_key ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'No license key found. Please configure your Progress Planner license key in settings.', 'progress-planner' ) ] ); + } + $result = \progress_planner()->get_ai_tasks()->execute_ai_task( $task_id, $site_url, $license_key ); if ( \is_wp_error( $result ) ) { @@ -217,37 +231,48 @@ public function handle_ai_task_execution() { public function add_popover() { ?>
-
-

- -
-
-
- - get_admin__enqueue()->enqueue_script( + 'progress-planner/web-components/prpl-ai-task-popover', + [ + 'file' => 'web-components/prpl-ai-task-popover.js', + 'dependencies' => [ 'progress-planner/web-components/prpl-interactive-task' ], + ] + ); + // Enqueue the AI task script. \progress_planner()->get_admin__enqueue()->enqueue_script( 'progress-planner/recommendations/ai-task', [ 'file' => 'recommendations/ai-task.js', - 'dependencies' => [ 'progress-planner/suggested-task' ], + 'dependencies' => [ 'progress-planner/suggested-task', 'progress-planner/web-components/prpl-ai-task-popover' ], ] ); @@ -291,10 +325,13 @@ public function enqueue_scripts( $hook ) { * @return array */ public function add_task_actions( $data = [], $actions = [] ) { + // Get the task ID from meta - REST API strips the prpl_ prefix. + $task_id = 6149; //$data[0]['task_id'] ?? ''; + // Add the "Analyze" button that opens the popover. $actions[] = [ 'priority' => 10, - 'html' => '', + 'html' => '', ]; return $actions; From 16b2a05d6aade83fed30ed67c28e82bfb8e8d364 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 30 Oct 2025 08:42:01 +0000 Subject: [PATCH 3/8] more ai changes Co-authored-by: Sculptor --- .../js/web-components/prpl-ai-task-popover.js | 34 ++- classes/class-ai-tasks.php | 60 +---- .../providers/class-ai-task.php | 230 ------------------ .../providers/class-ai-tasks-from-server.php | 96 +++++--- 4 files changed, 96 insertions(+), 324 deletions(-) delete mode 100644 classes/suggested-tasks/providers/class-ai-task.php diff --git a/assets/js/web-components/prpl-ai-task-popover.js b/assets/js/web-components/prpl-ai-task-popover.js index 540e916bd..6318b0f72 100644 --- a/assets/js/web-components/prpl-ai-task-popover.js +++ b/assets/js/web-components/prpl-ai-task-popover.js @@ -1,4 +1,4 @@ -/* global HTMLElement, prplSuggestedTask, customElements, PrplInteractiveTask */ +/* global prplSuggestedTask, customElements, PrplInteractiveTask */ /** * AI Task Popover Web Component @@ -20,7 +20,9 @@ customElements.define( attachDefaultEventListeners() { // Prevent attaching listeners multiple times if ( this.listenersAttached ) { - console.log( 'AI Task: Event listeners already attached, skipping' ); + console.log( + 'AI Task: Event listeners already attached, skipping' + ); return; } @@ -50,7 +52,9 @@ customElements.define( // Prevent multiple completions if ( this.isCompleting ) { - console.warn( 'AI Task: Already completing, ignoring duplicate call' ); + console.warn( + 'AI Task: Already completing, ignoring duplicate call' + ); return; } @@ -71,17 +75,26 @@ customElements.define( '#prpl-suggested-tasks-list .prpl-suggested-task' ); - console.log( 'AI Task: Found', tasks.length, 'task elements in list' ); + console.log( + 'AI Task: Found', + tasks.length, + 'task elements in list' + ); let foundMatch = false; tasks.forEach( ( taskElement ) => { - console.log( 'AI Task: Checking task element with ID:', taskElement.dataset.taskId ); + console.log( + 'AI Task: Checking task element with ID:', + taskElement.dataset.taskId + ); if ( taskElement.dataset.taskId === currentTaskId ) { console.log( 'AI Task: Found matching task element' ); if ( foundMatch ) { - console.warn( 'AI Task: Found duplicate matching task element! This should not happen.' ); + console.warn( + 'AI Task: Found duplicate matching task element! This should not happen.' + ); return; } @@ -97,7 +110,10 @@ customElements.define( const postId = parseInt( taskElement.dataset.postId ); if ( postId ) { - console.log( 'AI Task: Calling maybeComplete with post ID:', postId ); + console.log( + 'AI Task: Calling maybeComplete with post ID:', + postId + ); prplSuggestedTask.maybeComplete( postId ); // Reset flag after a delay @@ -105,7 +121,9 @@ customElements.define( this.isCompleting = false; }, 1000 ); } else { - console.error( 'AI Task: No post ID found on task element' ); + console.error( + 'AI Task: No post ID found on task element' + ); this.isCompleting = false; } } diff --git a/classes/class-ai-tasks.php b/classes/class-ai-tasks.php index 7e03d897a..525540018 100644 --- a/classes/class-ai-tasks.php +++ b/classes/class-ai-tasks.php @@ -76,60 +76,6 @@ public function get_items( $args = [] ) { return $json; } - /** - * Execute an AI task. - * - * @param int $task_id The task ID to execute. - * @param string $site_url The site URL to analyze. - * @param string $license_key The license key. - * @return array|WP_Error Response array on success, WP_Error on failure. - */ - public function execute_ai_task( $task_id, $site_url, $license_key ) { - $url = \progress_planner()->get_remote_server_root_url() . '/wp-json/progress-planner-saas/v1/execute-ai-task'; - - // Testing. - $site_url = 'https://progressplanner.com'; - - $post_data = [ - 'task_id' => $task_id, - 'site_url' => $site_url, - 'license_key' => $license_key, - ]; - - $response = \wp_remote_post( - $url, - [ - 'body' => $post_data, - 'timeout' => 45, // AI tasks may take longer. - ] - ); - - if ( \is_wp_error( $response ) ) { - \error_log( 'WP_Error response: ' . $response->get_error_message() ); - return $response; - } - - $response_code = \wp_remote_retrieve_response_code( $response ); - $body = \wp_remote_retrieve_body( $response ); - $json = \json_decode( $body, true ); - - // Debug logging for response. - \error_log( 'SaaS server response:' ); - \error_log( ' Response code: ' . $response_code ); - \error_log( ' Response body: ' . $body ); - - if ( 200 !== $response_code ) { - $error_message = isset( $json['message'] ) ? $json['message'] : 'Unknown error occurred'; - return new \WP_Error( 'ai_execution_failed', $error_message, [ 'status' => $response_code ] ); - } - - if ( ! \is_array( $json ) ) { - return new \WP_Error( 'invalid_response', 'Invalid response from server', [ 'status' => 500 ] ); - } - - return $json; - } - /** * Get a cached AI response for a task. * @@ -138,7 +84,7 @@ public function execute_ai_task( $task_id, $site_url, $license_key ) { */ public function get_cached_response( $task_id ) { $cache_key = 'prpl_ai_response_' . $task_id; - return progress_planner()->get_utils__cache()->get( $cache_key ); + return \progress_planner()->get_utils__cache()->get( $cache_key ); } /** @@ -151,7 +97,7 @@ public function get_cached_response( $task_id ) { */ public function cache_response( $task_id, $response, $expiry = WEEK_IN_SECONDS ) { $cache_key = 'prpl_ai_response_' . $task_id; - return progress_planner()->get_utils__cache()->set( $cache_key, $response, $expiry ); + return \progress_planner()->get_utils__cache()->set( $cache_key, $response, $expiry ); } /** @@ -162,6 +108,6 @@ public function cache_response( $task_id, $response, $expiry = WEEK_IN_SECONDS ) */ public function clear_cached_response( $task_id ) { $cache_key = 'prpl_ai_response_' . $task_id; - return progress_planner()->get_utils__cache()->delete( $cache_key ); + return \progress_planner()->get_utils__cache()->delete( $cache_key ); } } diff --git a/classes/suggested-tasks/providers/class-ai-task.php b/classes/suggested-tasks/providers/class-ai-task.php deleted file mode 100644 index fd19a3ef6..000000000 --- a/classes/suggested-tasks/providers/class-ai-task.php +++ /dev/null @@ -1,230 +0,0 @@ -ai_task_data = $ai_task_data; - } - - /** - * Get the task title. - * - * @return string - */ - protected function get_title() { - return $this->ai_task_data['title'] ?? ''; - } - - /** - * Get the task description. - * - * @return string - */ - protected function get_description() { - return $this->ai_task_data['description'] ?? 'Click "Analyze" to get AI-powered insights.'; - } - - /** - * Check if this should be added as a task. - * AI tasks from server should always be added. - * - * @return bool - */ - public function should_add_task() { - // AI tasks are managed by the SaaS server, so we always show them if they exist. - return ! empty( $this->ai_task_data ); - } - - /** - * Get the task ID. - * - * @param array $task_data Optional data to include in the task ID. - * @return string - */ - public function get_task_id( $task_data = [] ) { - $server_task_id = $this->ai_task_data['task_id'] ?? 0; - return 'ai-task-' . $server_task_id; - } - - /** - * Get the task details. - * - * @param array $task_data The task data. - * - * @return array - */ - public function get_task_details( $task_data = [] ) { - $details = parent::get_task_details( $task_data ); - - // Add AI-specific metadata. - $details['is_ai_task'] = true; - $details['ai_task_id'] = $this->ai_task_data['task_id'] ?? 0; - $details['ai_prompt_template'] = $this->ai_task_data['ai_prompt_template'] ?? ''; - $details['ai_provider'] = $this->ai_task_data['ai_provider'] ?? 'chatgpt'; - $details['ai_max_tokens'] = $this->ai_task_data['ai_max_tokens'] ?? 500; - $details['branding'] = $this->ai_task_data['branding'] ?? ''; - - return $details; - } - - /** - * Handle AI task execution via AJAX. - * - * @return void - */ - public function handle_ai_task_execution() { - // Check if the user has the necessary capabilities. - if ( ! \current_user_can( 'manage_options' ) ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'You do not have permission to execute AI tasks.', 'progress-planner' ) ] ); - } - - // Check the nonce. - if ( ! \check_ajax_referer( 'progress_planner', 'nonce', false ) ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid nonce.', 'progress-planner' ) ] ); - } - - if ( ! isset( $_POST['task_id'] ) ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'Missing task ID.', 'progress-planner' ) ] ); - } - - $task_id = (int) \sanitize_text_field( \wp_unslash( $_POST['task_id'] ) ); - - // Check if we have a cached response. - $cached_response = \progress_planner()->get_ai_tasks()->get_cached_response( $task_id ); - if ( false !== $cached_response ) { - \wp_send_json_success( - [ - 'success' => true, - 'task_id' => $task_id, - 'ai_response' => $cached_response, - 'cached' => true, - ] - ); - } - - // Execute the AI task via the SaaS server. - $site_url = \get_site_url(); - $license_key = \get_option( 'progress_planner_license_key' ); - - $result = \progress_planner()->get_ai_tasks()->execute_ai_task( $task_id, $site_url, $license_key ); - - if ( \is_wp_error( $result ) ) { - \wp_send_json_error( - [ - 'message' => $result->get_error_message(), - 'code' => $result->get_error_code(), - ] - ); - } - - // Cache the response. - if ( isset( $result['ai_response'] ) ) { - \progress_planner()->get_ai_tasks()->cache_response( $task_id, $result['ai_response'] ); - } - - \wp_send_json_success( $result ); - } - - /** - * Get the list of allowed options that can be updated via interactive tasks. - * - * @return array List of allowed option names. - */ - protected function get_allowed_interactive_options() { - // AI tasks don't update options via the standard interactive form. - return []; - } - - /** - * Print the popover form contents. - * - * @return void - */ - public function print_popover_form_contents() { - $task_id = $this->ai_task_data['task_id'] ?? 0; - ?> -
- - - -
-
- - -
- 'recommendations/ai-task.js', - 'dependencies' => [ 'progress-planner/suggested-task' ], - ]; - } - - /** - * Add task actions for AI tasks. - * - * @param array $data The task data. - * @param array $actions The existing actions. - * - * @return array - */ - public function add_task_actions( $data = [], $actions = [] ) { - // Add the "Analyze" button that opens the popover. - $actions[] = [ - 'priority' => 10, - 'html' => '', - ]; - - return $actions; - } -} diff --git a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php index 3faacfff7..1a66ecbd6 100644 --- a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php +++ b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php @@ -10,7 +10,7 @@ /** * Fetches and manages AI-powered tasks from the SaaS server. */ -class AI_Tasks_From_Server extends Tasks { +class AI_Tasks_From_Server extends Tasks_Interactive { /** * The ID of the task provider. @@ -31,7 +31,7 @@ class AI_Tasks_From_Server extends Tasks { * * @var string */ - protected const POPOVER_ID = 'ai-task'; + const POPOVER_ID = 'ai-task'; /** * Priority for AI tasks (higher priority = shown earlier). @@ -69,21 +69,14 @@ class AI_Tasks_From_Server extends Tasks { protected $ai_tasks = []; /** - * Constructor. + * Initialize the task provider. */ - public function __construct() { + public function init() { // Fetch AI tasks from server. $this->ai_tasks = \progress_planner()->get_ai_tasks()->get_items(); // Add AJAX handler for AI task execution. \add_action( 'wp_ajax_prpl_execute_ai_task', [ $this, 'handle_ai_task_execution' ] ); - - // Add popover for AI tasks. - \add_action( 'progress_planner_admin_page_after_widgets', [ $this, 'add_popover' ] ); - \add_action( 'progress_planner_admin_dashboard_widget_score_after', [ $this, 'add_popover' ] ); - - // Enqueue AI task scripts. - \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); } /** @@ -187,24 +180,7 @@ public function handle_ai_task_execution() { } // Execute the AI task via the SaaS server. - $site_url = \get_site_url(); - $license_key = \get_option( 'progress_planner_license_key' ); - - // Debug logging. - \error_log( 'AI Task Execution Debug:' ); - \error_log( ' Task ID: ' . $task_id ); - \error_log( ' Site URL: ' . $site_url ); - \error_log( ' License key exists: ' . ( ! empty( $license_key ) ? 'yes' : 'no' ) ); - \error_log( ' License key length: ' . \strlen( $license_key ) ); - \error_log( ' License key first 10 chars: ' . \substr( $license_key, 0, 10 ) . '...' ); - \error_log( ' License key type: ' . \gettype( $license_key ) ); - - // Validate we have the required data. - if ( empty( $license_key ) ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'No license key found. Please configure your Progress Planner license key in settings.', 'progress-planner' ) ] ); - } - - $result = \progress_planner()->get_ai_tasks()->execute_ai_task( $task_id, $site_url, $license_key ); + $result = $this->execute_ai_task( $task_id ); if ( \is_wp_error( $result ) ) { \wp_send_json_error( @@ -223,8 +199,70 @@ public function handle_ai_task_execution() { \wp_send_json_success( $result ); } + /** + * Execute an AI task. + * + * @param int $task_id The task ID to execute. + * @return array|\WP_Error Response array on success, WP_Error on failure. + */ + protected function execute_ai_task( $task_id ) { + $url = \progress_planner()->get_remote_server_root_url() . '/wp-json/progress-planner-saas/v1/execute-ai-task'; + + $site_url = \get_site_url(); + $license_key = \get_option( 'progress_planner_license_key' ); + + // Validate we have the required data. + if ( empty( $license_key ) ) { + return new \WP_Error( 'no_license_key', \esc_html__( 'No license key found. Please configure your Progress Planner license key in settings.', 'progress-planner' ) ); + } + + $post_data = [ + 'task_id' => $task_id, + 'site_url' => $site_url, + 'license_key' => $license_key, + ]; + + $response = \wp_remote_post( + $url, + [ + 'body' => $post_data, + 'timeout' => 45, // AI tasks may take longer. + ] + ); + + if ( \is_wp_error( $response ) ) { + return $response; + } + + $response_code = \wp_remote_retrieve_response_code( $response ); + $body = \wp_remote_retrieve_body( $response ); + $json = \json_decode( $body, true ); + + if ( 200 !== $response_code ) { + $error_message = isset( $json['message'] ) ? $json['message'] : 'Unknown error occurred'; + return new \WP_Error( 'ai_execution_failed', $error_message, [ 'status' => $response_code ] ); + } + + if ( ! \is_array( $json ) ) { + return new \WP_Error( 'invalid_response', 'Invalid response from server', [ 'status' => 500 ] ); + } + + return $json; + } + + /** + * Print popover form contents. + * Required by Tasks_Interactive, but we override add_popover() completely. + * + * @return void + */ + public function print_popover_form_contents() { + // Not used - we override add_popover() completely. + } + /** * Add the popover for AI tasks. + * Overrides parent to provide custom popover structure for AI tasks. * * @return void */ From 373df4ac2db448162206234e5c84e6a47cdd39c5 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 30 Oct 2025 10:07:18 +0000 Subject: [PATCH 4/8] small refactor Co-authored-by: Sculptor --- assets/js/recommendations/ai-task.js | 58 ++++++++++++++----- classes/class-suggested-tasks.php | 21 +------ .../providers/class-ai-tasks-from-server.php | 14 ++--- 3 files changed, 54 insertions(+), 39 deletions(-) diff --git a/assets/js/recommendations/ai-task.js b/assets/js/recommendations/ai-task.js index 2b99aec4a..d00ea3b59 100644 --- a/assets/js/recommendations/ai-task.js +++ b/assets/js/recommendations/ai-task.js @@ -20,8 +20,12 @@ const executeButton = popover.querySelector( '.prpl-ai-task-execute' ); const retryButton = popover.querySelector( '.prpl-ai-task-retry' ); - const completeButton = popover.querySelector( '.prpl-ai-task-complete' ); - const instructionsEl = popover.querySelector( '.prpl-ai-task-instructions' ); + const completeButton = popover.querySelector( + '.prpl-ai-task-complete' + ); + const instructionsEl = popover.querySelector( + '.prpl-ai-task-instructions' + ); const loadingEl = popover.querySelector( '.prpl-ai-task-loading' ); const resultEl = popover.querySelector( '.prpl-ai-task-result' ); const responseEl = popover.querySelector( '.prpl-ai-task-response' ); @@ -48,8 +52,8 @@ /** * Show result state. * - * @param {string} response - The AI response text. - * @param {boolean} cached - Whether the response was cached. + * @param {string} response - The AI response text. + * @param {boolean} cached - Whether the response was cached. */ const showResult = ( response, cached = false ) => { if ( instructionsEl ) { @@ -175,16 +179,26 @@ '.prpl-ai-task-trigger' ); - console.log( 'Found AI task trigger buttons:', triggerButtons.length ); + console.log( + 'Found AI task trigger buttons:', + triggerButtons.length + ); triggerButtons.forEach( ( button ) => { button.addEventListener( 'click', () => { const taskId = button.dataset.taskId; - console.log( 'Trigger button clicked, task ID from data attribute:', taskId ); + console.log( + 'Trigger button clicked, task ID from data attribute:', + taskId + ); // Get the task element and its slug for completion - const taskElement = button.closest( '.prpl-suggested-task' ); - const taskSlug = taskElement ? taskElement.dataset.taskId : null; + const taskElement = button.closest( + '.prpl-suggested-task' + ); + const taskSlug = taskElement + ? taskElement.dataset.taskId + : null; console.log( 'Task slug from element:', taskSlug ); if ( taskId ) { @@ -195,10 +209,18 @@ console.log( 'Set currentTaskId to:', currentTaskId ); // Store the task slug on the web component so completeTask can find it - const webComponent = popover.querySelector( 'prpl-ai-task-popover' ); + const webComponent = popover.querySelector( + 'prpl-ai-task-popover' + ); if ( webComponent && taskSlug ) { - webComponent.setAttribute( 'current-task-id', taskSlug ); - console.log( 'Set current-task-id on web component:', taskSlug ); + webComponent.setAttribute( + 'current-task-id', + taskSlug + ); + console.log( + 'Set current-task-id on web component:', + taskSlug + ); } // Reset the popover state. @@ -220,8 +242,15 @@ if ( executeButton ) { executeButton.addEventListener( 'click', () => { // Get task ID from button's data attribute as fallback - const taskId = currentTaskId || parseInt( executeButton.dataset.taskId ); - console.log( 'Execute button clicked, task ID:', taskId, '(currentTaskId:', currentTaskId, ')' ); + const taskId = + currentTaskId || parseInt( executeButton.dataset.taskId ); + console.log( + 'Execute button clicked, task ID:', + taskId, + '(currentTaskId:', + currentTaskId, + ')' + ); if ( taskId ) { executeTask( taskId ); } else { @@ -233,7 +262,8 @@ // Retry button click handler. if ( retryButton ) { retryButton.addEventListener( 'click', () => { - const taskId = currentTaskId || parseInt( retryButton.dataset.taskId ); + const taskId = + currentTaskId || parseInt( retryButton.dataset.taskId ); if ( taskId ) { executeTask( taskId ); } diff --git a/classes/class-suggested-tasks.php b/classes/class-suggested-tasks.php index b87d2ff2d..3a3efcdd2 100644 --- a/classes/class-suggested-tasks.php +++ b/classes/class-suggested-tasks.php @@ -386,33 +386,18 @@ public function register_post_type() { 'show_in_rest' => true, 'default' => 0, ], - 'is_ai_task' => [ + 'prpl_is_ai_task' => [ 'type' => 'boolean', 'single' => true, 'show_in_rest' => true, 'default' => false, ], - 'ai_task_server_id' => [ + 'prpl_ai_task_server_id' => [ 'type' => 'number', 'single' => true, 'show_in_rest' => true, ], - 'ai_prompt_template' => [ - 'type' => 'string', - 'single' => true, - 'show_in_rest' => true, - ], - 'ai_provider' => [ - 'type' => 'string', - 'single' => true, - 'show_in_rest' => true, - ], - 'ai_max_tokens' => [ - 'type' => 'number', - 'single' => true, - 'show_in_rest' => true, - ], - 'branding' => [ + 'prpl_ai_prompt_template' => [ 'type' => 'string', 'single' => true, 'show_in_rest' => true, diff --git a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php index 1a66ecbd6..727c7410f 100644 --- a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php +++ b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php @@ -133,9 +133,6 @@ public function get_tasks_to_inject() { 'is_ai_task' => true, 'ai_task_server_id' => $ai_task['task_id'], 'ai_prompt_template' => $ai_task['ai_prompt_template'] ?? '', - 'ai_provider' => $ai_task['ai_provider'] ?? 'chatgpt', - 'ai_max_tokens' => $ai_task['ai_max_tokens'] ?? 500, - 'branding' => $ai_task['branding'] ?? '', ]; $tasks_to_inject[] = \progress_planner()->get_suggested_tasks_db()->add( $task_data ); @@ -208,7 +205,6 @@ public function handle_ai_task_execution() { protected function execute_ai_task( $task_id ) { $url = \progress_planner()->get_remote_server_root_url() . '/wp-json/progress-planner-saas/v1/execute-ai-task'; - $site_url = \get_site_url(); $license_key = \get_option( 'progress_planner_license_key' ); // Validate we have the required data. @@ -218,7 +214,7 @@ protected function execute_ai_task( $task_id ) { $post_data = [ 'task_id' => $task_id, - 'site_url' => $site_url, + 'site_url' => \get_site_url(), 'license_key' => $license_key, ]; @@ -363,8 +359,12 @@ public function enqueue_scripts( $hook ) { * @return array */ public function add_task_actions( $data = [], $actions = [] ) { - // Get the task ID from meta - REST API strips the prpl_ prefix. - $task_id = 6149; //$data[0]['task_id'] ?? ''; + + $task_id = $data['meta']['prpl_ai_task_server_id'] ?? ''; + + if ( ! $task_id ) { + return $actions; + } // Add the "Analyze" button that opens the popover. $actions[] = [ From 0a18fd0ff97cf4b28dab1bfc9de3a73422212672 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 30 Oct 2025 11:42:59 +0100 Subject: [PATCH 5/8] consistency --- assets/css/ai-task.css | 2 + assets/js/recommendations/ai-task.js | 98 ++----------------- classes/class-ai-tasks.php | 8 +- .../providers/class-ai-tasks-from-server.php | 24 ++++- 4 files changed, 37 insertions(+), 95 deletions(-) diff --git a/assets/css/ai-task.css b/assets/css/ai-task.css index b690e4aee..c61e1c9d3 100644 --- a/assets/css/ai-task.css +++ b/assets/css/ai-task.css @@ -30,6 +30,7 @@ } @keyframes prpl-spin { + to { transform: rotate(360deg); } @@ -101,6 +102,7 @@ /* Responsive */ @media (max-width: 768px) { + .prpl-popover-ai-task { max-width: 90vw; } diff --git a/assets/js/recommendations/ai-task.js b/assets/js/recommendations/ai-task.js index d00ea3b59..51e9b2cf5 100644 --- a/assets/js/recommendations/ai-task.js +++ b/assets/js/recommendations/ai-task.js @@ -175,54 +175,16 @@ * Set up event listeners for trigger buttons in task list. */ const setupTriggerButtons = () => { - const triggerButtons = document.querySelectorAll( - '.prpl-ai-task-trigger' - ); - - console.log( - 'Found AI task trigger buttons:', - triggerButtons.length - ); - - triggerButtons.forEach( ( button ) => { - button.addEventListener( 'click', () => { - const taskId = button.dataset.taskId; - console.log( - 'Trigger button clicked, task ID from data attribute:', - taskId - ); - - // Get the task element and its slug for completion - const taskElement = button.closest( - '.prpl-suggested-task' - ); - const taskSlug = taskElement - ? taskElement.dataset.taskId - : null; - console.log( 'Task slug from element:', taskSlug ); - - if ( taskId ) { - // Store the task ID on the execute button - executeButton.dataset.taskId = taskId; - retryButton.dataset.taskId = taskId; - currentTaskId = parseInt( taskId ); + document.addEventListener( + 'prpl-interactive-task-ai-task', + ( event ) => { + const taskContext = event.detail; + console.log( 'AI task event received:', taskContext ); + + if ( taskContext.remote_task_id ) { + currentTaskId = parseInt( taskContext.remote_task_id ); console.log( 'Set currentTaskId to:', currentTaskId ); - // Store the task slug on the web component so completeTask can find it - const webComponent = popover.querySelector( - 'prpl-ai-task-popover' - ); - if ( webComponent && taskSlug ) { - webComponent.setAttribute( - 'current-task-id', - taskSlug - ); - console.log( - 'Set current-task-id on web component:', - taskSlug - ); - } - // Reset the popover state. if ( instructionsEl ) { instructionsEl.style.display = 'block'; @@ -234,49 +196,9 @@ retryButton.style.display = 'none'; completeButton.style.display = 'none'; } - } ); - } ); - }; - - // Execute button click handler. - if ( executeButton ) { - executeButton.addEventListener( 'click', () => { - // Get task ID from button's data attribute as fallback - const taskId = - currentTaskId || parseInt( executeButton.dataset.taskId ); - console.log( - 'Execute button clicked, task ID:', - taskId, - '(currentTaskId:', - currentTaskId, - ')' - ); - if ( taskId ) { - executeTask( taskId ); - } else { - console.error( 'No task ID set' ); - } - } ); - } - - // Retry button click handler. - if ( retryButton ) { - retryButton.addEventListener( 'click', () => { - const taskId = - currentTaskId || parseInt( retryButton.dataset.taskId ); - if ( taskId ) { - executeTask( taskId ); } - } ); - } - - // Set up trigger buttons initially. - setupTriggerButtons(); - - // Re-setup trigger buttons when new tasks are injected. - document.addEventListener( 'prpl/suggestedTask/injectItem', () => { - setTimeout( setupTriggerButtons, 100 ); - } ); + ); + }; }; // Initialize when DOM is ready. diff --git a/classes/class-ai-tasks.php b/classes/class-ai-tasks.php index 525540018..e34e6185b 100644 --- a/classes/class-ai-tasks.php +++ b/classes/class-ai-tasks.php @@ -93,21 +93,21 @@ public function get_cached_response( $task_id ) { * @param int $task_id The task ID. * @param string $response The AI response. * @param int $expiry Optional. Cache expiry in seconds. Default 1 week. - * @return bool True on success, false on failure. + * @return void */ public function cache_response( $task_id, $response, $expiry = WEEK_IN_SECONDS ) { $cache_key = 'prpl_ai_response_' . $task_id; - return \progress_planner()->get_utils__cache()->set( $cache_key, $response, $expiry ); + \progress_planner()->get_utils__cache()->set( $cache_key, $response, $expiry ); } /** * Clear the cached AI response for a task. * * @param int $task_id The task ID. - * @return bool True on success, false on failure. + * @return void */ public function clear_cached_response( $task_id ) { $cache_key = 'prpl_ai_response_' . $task_id; - return \progress_planner()->get_utils__cache()->delete( $cache_key ); + \progress_planner()->get_utils__cache()->delete( $cache_key ); } } diff --git a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php index 727c7410f..407ce25cc 100644 --- a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php +++ b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php @@ -360,16 +360,34 @@ public function enqueue_scripts( $hook ) { */ public function add_task_actions( $data = [], $actions = [] ) { - $task_id = $data['meta']['prpl_ai_task_server_id'] ?? ''; + $remote_task_id = $data['meta']['prpl_ai_task_server_id'] ?? ''; - if ( ! $task_id ) { + if ( ! $remote_task_id ) { return $actions; } // Add the "Analyze" button that opens the popover. $actions[] = [ 'priority' => 10, - 'html' => '', + 'html' => \sprintf( + ' + %s + ', + \htmlspecialchars( + \wp_json_encode( + [ + 'remote_task_id' => $remote_task_id, + 'task_prompt' => $data['meta']['prpl_ai_prompt_template'] ?? '', + ] + ), + ENT_QUOTES, + 'UTF-8' + ), + \esc_attr( static::POPOVER_ID ), + \esc_html__( 'Analyze', 'progress-planner' ) + ), ]; return $actions; From 4c9ca313c0227ffeafb7db0116f16656ae616b26 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 30 Oct 2025 10:54:05 +0000 Subject: [PATCH 6/8] more consistency Co-authored-by: Sculptor --- assets/js/recommendations/ai-task.js | 190 +++++++----------- .../js/web-components/prpl-ai-task-popover.js | 101 ++++++++++ .../providers/class-ai-tasks-from-server.php | 6 +- 3 files changed, 182 insertions(+), 115 deletions(-) diff --git a/assets/js/recommendations/ai-task.js b/assets/js/recommendations/ai-task.js index d00ea3b59..27430ced1 100644 --- a/assets/js/recommendations/ai-task.js +++ b/assets/js/recommendations/ai-task.js @@ -5,7 +5,7 @@ * * Handles execution of AI-powered tasks from the SaaS server. * - * Dependencies: progress-planner/suggested-task, progress-planner/ajax-request + * Dependencies: progress-planner/suggested-task, progress-planner/ajax-request, progress-planner/web-components/prpl-ai-task-popover */ ( () => { @@ -18,6 +18,7 @@ return; } + const webComponent = popover.querySelector( 'prpl-ai-task-popover' ); const executeButton = popover.querySelector( '.prpl-ai-task-execute' ); const retryButton = popover.querySelector( '.prpl-ai-task-retry' ); const completeButton = popover.querySelector( @@ -32,21 +33,31 @@ const errorEl = popover.querySelector( '.prpl-ai-task-error' ); const errorMessageEl = popover.querySelector( '.prpl-error-message' ); - let currentTaskId = null; - /** * Show loading state. */ const showLoading = () => { - executeButton.style.display = 'none'; - retryButton.style.display = 'none'; - completeButton.style.display = 'none'; + if ( executeButton ) { + executeButton.style.display = 'none'; + } + if ( retryButton ) { + retryButton.style.display = 'none'; + } + if ( completeButton ) { + completeButton.style.display = 'none'; + } if ( instructionsEl ) { instructionsEl.style.display = 'none'; } - loadingEl.style.display = 'block'; - resultEl.style.display = 'none'; - errorEl.style.display = 'none'; + if ( loadingEl ) { + loadingEl.style.display = 'block'; + } + if ( resultEl ) { + resultEl.style.display = 'none'; + } + if ( errorEl ) { + errorEl.style.display = 'none'; + } }; /** @@ -59,19 +70,33 @@ if ( instructionsEl ) { instructionsEl.style.display = 'none'; } - loadingEl.style.display = 'none'; - executeButton.style.display = 'none'; - retryButton.style.display = 'none'; - errorEl.style.display = 'none'; - resultEl.style.display = 'block'; - completeButton.style.display = 'inline-block'; + if ( loadingEl ) { + loadingEl.style.display = 'none'; + } + if ( executeButton ) { + executeButton.style.display = 'none'; + } + if ( retryButton ) { + retryButton.style.display = 'none'; + } + if ( errorEl ) { + errorEl.style.display = 'none'; + } + if ( resultEl ) { + resultEl.style.display = 'block'; + } + if ( completeButton ) { + completeButton.style.display = 'inline-block'; + } // Format the response with markdown-like formatting. const formattedResponse = formatAIResponse( response ); - responseEl.innerHTML = formattedResponse; + if ( responseEl ) { + responseEl.innerHTML = formattedResponse; + } // Add cached indicator if applicable. - if ( cached ) { + if ( cached && responseEl ) { const cachedIndicator = document.createElement( 'p' ); cachedIndicator.className = 'prpl-ai-cached-indicator'; cachedIndicator.style.fontSize = '0.9em'; @@ -91,13 +116,27 @@ if ( instructionsEl ) { instructionsEl.style.display = 'none'; } - loadingEl.style.display = 'none'; - executeButton.style.display = 'none'; - retryButton.style.display = 'inline-block'; - completeButton.style.display = 'none'; - resultEl.style.display = 'none'; - errorEl.style.display = 'block'; - errorMessageEl.textContent = message; + if ( loadingEl ) { + loadingEl.style.display = 'none'; + } + if ( executeButton ) { + executeButton.style.display = 'none'; + } + if ( retryButton ) { + retryButton.style.display = 'inline-block'; + } + if ( completeButton ) { + completeButton.style.display = 'none'; + } + if ( resultEl ) { + resultEl.style.display = 'none'; + } + if ( errorEl ) { + errorEl.style.display = 'block'; + } + if ( errorMessageEl ) { + errorMessageEl.textContent = message; + } }; /** @@ -136,7 +175,6 @@ return; } - currentTaskId = taskId; showLoading(); // Make AJAX request to execute the AI task. @@ -171,90 +209,18 @@ } ); }; - /** - * Set up event listeners for trigger buttons in task list. - */ - const setupTriggerButtons = () => { - const triggerButtons = document.querySelectorAll( - '.prpl-ai-task-trigger' - ); - - console.log( - 'Found AI task trigger buttons:', - triggerButtons.length - ); - - triggerButtons.forEach( ( button ) => { - button.addEventListener( 'click', () => { - const taskId = button.dataset.taskId; - console.log( - 'Trigger button clicked, task ID from data attribute:', - taskId - ); - - // Get the task element and its slug for completion - const taskElement = button.closest( - '.prpl-suggested-task' - ); - const taskSlug = taskElement - ? taskElement.dataset.taskId - : null; - console.log( 'Task slug from element:', taskSlug ); - - if ( taskId ) { - // Store the task ID on the execute button - executeButton.dataset.taskId = taskId; - retryButton.dataset.taskId = taskId; - currentTaskId = parseInt( taskId ); - console.log( 'Set currentTaskId to:', currentTaskId ); - - // Store the task slug on the web component so completeTask can find it - const webComponent = popover.querySelector( - 'prpl-ai-task-popover' - ); - if ( webComponent && taskSlug ) { - webComponent.setAttribute( - 'current-task-id', - taskSlug - ); - console.log( - 'Set current-task-id on web component:', - taskSlug - ); - } - - // Reset the popover state. - if ( instructionsEl ) { - instructionsEl.style.display = 'block'; - } - loadingEl.style.display = 'none'; - resultEl.style.display = 'none'; - errorEl.style.display = 'none'; - executeButton.style.display = 'inline-block'; - retryButton.style.display = 'none'; - completeButton.style.display = 'none'; - } - } ); - } ); - }; - - // Execute button click handler. + // Execute button click handler - get task ID from web component attribute. if ( executeButton ) { executeButton.addEventListener( 'click', () => { - // Get task ID from button's data attribute as fallback const taskId = - currentTaskId || parseInt( executeButton.dataset.taskId ); - console.log( - 'Execute button clicked, task ID:', - taskId, - '(currentTaskId:', - currentTaskId, - ')' - ); + webComponent?.getAttribute( 'data-task-id' ) || + executeButton.dataset.taskId; + console.log( 'Execute button clicked, task ID:', taskId ); if ( taskId ) { - executeTask( taskId ); + executeTask( parseInt( taskId ) ); } else { - console.error( 'No task ID set' ); + console.error( 'No task ID found' ); + showError( 'Task ID not found. Please try again.' ); } } ); } @@ -263,20 +229,16 @@ if ( retryButton ) { retryButton.addEventListener( 'click', () => { const taskId = - currentTaskId || parseInt( retryButton.dataset.taskId ); + webComponent?.getAttribute( 'data-task-id' ) || + retryButton.dataset.taskId; if ( taskId ) { - executeTask( taskId ); + executeTask( parseInt( taskId ) ); + } else { + console.error( 'No task ID found' ); + showError( 'Task ID not found. Please try again.' ); } } ); } - - // Set up trigger buttons initially. - setupTriggerButtons(); - - // Re-setup trigger buttons when new tasks are injected. - document.addEventListener( 'prpl/suggestedTask/injectItem', () => { - setTimeout( setupTriggerButtons, 100 ); - } ); }; // Initialize when DOM is ready. diff --git a/assets/js/web-components/prpl-ai-task-popover.js b/assets/js/web-components/prpl-ai-task-popover.js index 6318b0f72..d7a49b1a6 100644 --- a/assets/js/web-components/prpl-ai-task-popover.js +++ b/assets/js/web-components/prpl-ai-task-popover.js @@ -13,6 +13,107 @@ customElements.define( this.listenersAttached = false; } + /** + * Runs when the popover is opening. + * Use this to set up task-specific data. + */ + popoverOpening() { + super.popoverOpening(); + + console.log( 'AI Task: Popover opening' ); + + // Get task data from the trigger button + const popoverId = this.getAttribute( 'popover-id' ); + const popover = document.getElementById( popoverId ); + + // Find the trigger button that was just clicked + // When a button with popovertarget is clicked, it becomes the activeElement + let triggerButton = document.activeElement; + + // Verify it's the correct button (has the matching popovertarget) + if ( ! triggerButton || triggerButton.getAttribute( 'popovertarget' ) !== popoverId ) { + // Fallback: search for the button within the closest task element if we can find one + console.warn( 'AI Task: Could not determine which trigger button was clicked via activeElement' ); + triggerButton = document.querySelector( + `button[popovertarget="${ popoverId }"]` + ); + } + + if ( ! triggerButton ) { + console.warn( 'AI Task: Could not find trigger button' ); + return; + } + + console.log( 'AI Task: Found trigger button:', triggerButton ); + + // Get task data from button attributes + const taskId = triggerButton.dataset.taskId; + const taskPrompt = triggerButton.dataset.taskPrompt; + + // Get the task element and its slug for completion + const taskElement = triggerButton.closest( '.prpl-suggested-task' ); + const taskSlug = taskElement ? taskElement.dataset.taskId : null; + + console.log( 'AI Task: Task ID:', taskId ); + console.log( 'AI Task: Task slug:', taskSlug ); + console.log( 'AI Task: Task prompt:', taskPrompt ); + + // Store task data on web component + this.setAttribute( 'data-task-id', taskId ); + this.setAttribute( 'current-task-id', taskSlug ); + + // Display task prompt if available + const promptEl = popover.querySelector( '.prpl-ai-task-prompt' ); + const promptTextEl = popover.querySelector( + '.prpl-ai-task-prompt-text' + ); + if ( taskPrompt && promptTextEl ) { + promptTextEl.textContent = taskPrompt; + promptEl.style.display = 'block'; + } else if ( promptEl ) { + promptEl.style.display = 'none'; + } + + // Reset popover state + const instructionsEl = popover.querySelector( + '.prpl-ai-task-instructions' + ); + const loadingEl = popover.querySelector( '.prpl-ai-task-loading' ); + const resultEl = popover.querySelector( '.prpl-ai-task-result' ); + const errorEl = popover.querySelector( '.prpl-ai-task-error' ); + const executeButton = popover.querySelector( + '.prpl-ai-task-execute' + ); + const retryButton = popover.querySelector( '.prpl-ai-task-retry' ); + const completeButton = popover.querySelector( + '.prpl-ai-task-complete' + ); + + if ( instructionsEl ) { + instructionsEl.style.display = 'block'; + } + if ( loadingEl ) { + loadingEl.style.display = 'none'; + } + if ( resultEl ) { + resultEl.style.display = 'none'; + } + if ( errorEl ) { + errorEl.style.display = 'none'; + } + if ( executeButton ) { + executeButton.style.display = 'inline-block'; + executeButton.dataset.taskId = taskId; + } + if ( retryButton ) { + retryButton.style.display = 'none'; + retryButton.dataset.taskId = taskId; + } + if ( completeButton ) { + completeButton.style.display = 'none'; + } + } + /** * Attach button event listeners. * Overrides parent to prevent duplicate listeners. diff --git a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php index 727c7410f..5d89b496f 100644 --- a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php +++ b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php @@ -280,6 +280,9 @@ public function add_popover() {
+

@@ -361,6 +364,7 @@ public function enqueue_scripts( $hook ) { public function add_task_actions( $data = [], $actions = [] ) { $task_id = $data['meta']['prpl_ai_task_server_id'] ?? ''; + $task_prompt = $data['meta']['prpl_ai_prompt_template'] ?? ''; if ( ! $task_id ) { return $actions; @@ -369,7 +373,7 @@ public function add_task_actions( $data = [], $actions = [] ) { // Add the "Analyze" button that opens the popover. $actions[] = [ 'priority' => 10, - 'html' => '', + 'html' => '', ]; return $actions; From 8763535a1af22c04107a02004076d37a57c67907 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 30 Oct 2025 12:00:14 +0000 Subject: [PATCH 7/8] more consistency Co-authored-by: Sculptor --- assets/js/recommendations/ai-task.js | 393 ++++++++++++------ .../js/web-components/prpl-ai-task-popover.js | 165 +------- .../providers/class-ai-tasks-from-server.php | 26 +- 3 files changed, 297 insertions(+), 287 deletions(-) diff --git a/assets/js/recommendations/ai-task.js b/assets/js/recommendations/ai-task.js index 27430ced1..921bf4225 100644 --- a/assets/js/recommendations/ai-task.js +++ b/assets/js/recommendations/ai-task.js @@ -1,5 +1,4 @@ /* global progressPlannerAjaxRequest, progressPlanner */ - /** * AI Task Handler * @@ -7,58 +6,227 @@ * * Dependencies: progress-planner/suggested-task, progress-planner/ajax-request, progress-planner/web-components/prpl-ai-task-popover */ - -( () => { +( function () { /** - * Handle AI task execution. + * AI Task class. */ - const handleAITaskExecution = () => { - const popover = document.getElementById( 'prpl-popover-ai-task' ); - if ( ! popover ) { - return; + class AITask { + /** + * Constructor. + */ + constructor() { + this.popoverId = 'prpl-popover-ai-task'; + + // Early return if the popover is not found. + if ( ! document.getElementById( this.popoverId ) ) { + return; + } + + this.currentTaskData = null; + this.currentTaskElement = null; + this.elements = this.getElements(); + this.init(); + } + + /** + * Get all DOM elements. + * + * @return {Object} Object containing all DOM elements. + */ + getElements() { + const popover = document.getElementById( this.popoverId ); + return { + popover, + webComponent: popover.querySelector( 'prpl-ai-task-popover' ), + executeButton: popover.querySelector( '.prpl-ai-task-execute' ), + retryButton: popover.querySelector( '.prpl-ai-task-retry' ), + completeButton: popover.querySelector( + '.prpl-ai-task-complete' + ), + instructionsEl: popover.querySelector( + '.prpl-ai-task-instructions' + ), + loadingEl: popover.querySelector( '.prpl-ai-task-loading' ), + resultEl: popover.querySelector( '.prpl-ai-task-result' ), + responseEl: popover.querySelector( '.prpl-ai-task-response' ), + errorEl: popover.querySelector( '.prpl-ai-task-error' ), + errorMessageEl: popover.querySelector( '.prpl-error-message' ), + promptEl: popover.querySelector( '.prpl-ai-task-prompt' ), + promptTextEl: popover.querySelector( + '.prpl-ai-task-prompt-text' + ), + }; + } + + /** + * Initialize the component. + */ + init() { + this.bindEvents(); + } + + /** + * Bind event listeners. + */ + bindEvents() { + // Listen for the generic interactive task action event. + document.addEventListener( + 'prpl-interactive-task-action-ai-task', + ( event ) => { + this.handleInteractiveTaskAction( event ); + } + ); + + // Execute button click handler. + if ( this.elements.executeButton ) { + this.elements.executeButton.addEventListener( 'click', () => { + if ( this.currentTaskData?.taskId ) { + this.executeTask( + parseInt( this.currentTaskData.taskId ) + ); + } else { + console.error( 'No task ID found' ); + this.showError( + 'Task ID not found. Please try again.' + ); + } + } ); + } + + // Retry button click handler. + if ( this.elements.retryButton ) { + this.elements.retryButton.addEventListener( 'click', () => { + if ( this.currentTaskData?.taskId ) { + this.executeTask( + parseInt( this.currentTaskData.taskId ) + ); + } else { + console.error( 'No task ID found' ); + this.showError( + 'Task ID not found. Please try again.' + ); + } + } ); + } + } + + /** + * Handle interactive task action event. + * + * @param {CustomEvent} event The custom event with task context data. + */ + handleInteractiveTaskAction( event ) { + this.currentTaskData = { + taskId: this.decodeHtmlEntities( event.detail.remote_task_id ), + taskPrompt: this.decodeHtmlEntities( event.detail.task_prompt ), + }; + + // Store reference to the task element that triggered this. + this.currentTaskElement = event.target.closest( + '.prpl-suggested-task' + ); + + // Update the web component with current task data. + if ( this.elements.webComponent && this.currentTaskElement ) { + this.elements.webComponent.setAttribute( + 'data-task-id', + this.currentTaskData.taskId + ); + this.elements.webComponent.setAttribute( + 'current-task-id', + this.currentTaskElement.dataset.taskId + ); + } + + // Update the popover content with the task data. + this.updatePopoverContent( + this.currentTaskData.taskId, + this.currentTaskData.taskPrompt + ); + + // Reset popover state. + this.resetPopoverState(); } - const webComponent = popover.querySelector( 'prpl-ai-task-popover' ); - const executeButton = popover.querySelector( '.prpl-ai-task-execute' ); - const retryButton = popover.querySelector( '.prpl-ai-task-retry' ); - const completeButton = popover.querySelector( - '.prpl-ai-task-complete' - ); - const instructionsEl = popover.querySelector( - '.prpl-ai-task-instructions' - ); - const loadingEl = popover.querySelector( '.prpl-ai-task-loading' ); - const resultEl = popover.querySelector( '.prpl-ai-task-result' ); - const responseEl = popover.querySelector( '.prpl-ai-task-response' ); - const errorEl = popover.querySelector( '.prpl-ai-task-error' ); - const errorMessageEl = popover.querySelector( '.prpl-error-message' ); + /** + * Update the popover content. + * + * @param {string} taskId The task ID. + * @param {string} taskPrompt The task prompt. + */ + updatePopoverContent( taskId, taskPrompt ) { + // Display task prompt if available. + if ( taskPrompt && this.elements.promptTextEl ) { + this.elements.promptTextEl.textContent = taskPrompt; + if ( this.elements.promptEl ) { + this.elements.promptEl.style.display = 'block'; + } + } else if ( this.elements.promptEl ) { + this.elements.promptEl.style.display = 'none'; + } + + // Store task ID on buttons as backup. + if ( this.elements.executeButton ) { + this.elements.executeButton.dataset.taskId = taskId; + } + if ( this.elements.retryButton ) { + this.elements.retryButton.dataset.taskId = taskId; + } + } + + /** + * Reset popover state to initial view. + */ + resetPopoverState() { + if ( this.elements.instructionsEl ) { + this.elements.instructionsEl.style.display = 'block'; + } + if ( this.elements.loadingEl ) { + this.elements.loadingEl.style.display = 'none'; + } + if ( this.elements.resultEl ) { + this.elements.resultEl.style.display = 'none'; + } + if ( this.elements.errorEl ) { + this.elements.errorEl.style.display = 'none'; + } + if ( this.elements.executeButton ) { + this.elements.executeButton.style.display = 'inline-block'; + } + if ( this.elements.retryButton ) { + this.elements.retryButton.style.display = 'none'; + } + if ( this.elements.completeButton ) { + this.elements.completeButton.style.display = 'none'; + } + } /** * Show loading state. */ - const showLoading = () => { - if ( executeButton ) { - executeButton.style.display = 'none'; + showLoading() { + if ( this.elements.executeButton ) { + this.elements.executeButton.style.display = 'none'; } - if ( retryButton ) { - retryButton.style.display = 'none'; + if ( this.elements.retryButton ) { + this.elements.retryButton.style.display = 'none'; } - if ( completeButton ) { - completeButton.style.display = 'none'; + if ( this.elements.completeButton ) { + this.elements.completeButton.style.display = 'none'; } - if ( instructionsEl ) { - instructionsEl.style.display = 'none'; + if ( this.elements.instructionsEl ) { + this.elements.instructionsEl.style.display = 'none'; } - if ( loadingEl ) { - loadingEl.style.display = 'block'; + if ( this.elements.loadingEl ) { + this.elements.loadingEl.style.display = 'block'; } - if ( resultEl ) { - resultEl.style.display = 'none'; + if ( this.elements.resultEl ) { + this.elements.resultEl.style.display = 'none'; } - if ( errorEl ) { - errorEl.style.display = 'none'; + if ( this.elements.errorEl ) { + this.elements.errorEl.style.display = 'none'; } - }; + } /** * Show result state. @@ -66,78 +234,78 @@ * @param {string} response - The AI response text. * @param {boolean} cached - Whether the response was cached. */ - const showResult = ( response, cached = false ) => { - if ( instructionsEl ) { - instructionsEl.style.display = 'none'; + showResult( response, cached = false ) { + if ( this.elements.instructionsEl ) { + this.elements.instructionsEl.style.display = 'none'; } - if ( loadingEl ) { - loadingEl.style.display = 'none'; + if ( this.elements.loadingEl ) { + this.elements.loadingEl.style.display = 'none'; } - if ( executeButton ) { - executeButton.style.display = 'none'; + if ( this.elements.executeButton ) { + this.elements.executeButton.style.display = 'none'; } - if ( retryButton ) { - retryButton.style.display = 'none'; + if ( this.elements.retryButton ) { + this.elements.retryButton.style.display = 'none'; } - if ( errorEl ) { - errorEl.style.display = 'none'; + if ( this.elements.errorEl ) { + this.elements.errorEl.style.display = 'none'; } - if ( resultEl ) { - resultEl.style.display = 'block'; + if ( this.elements.resultEl ) { + this.elements.resultEl.style.display = 'block'; } - if ( completeButton ) { - completeButton.style.display = 'inline-block'; + if ( this.elements.completeButton ) { + this.elements.completeButton.style.display = 'inline-block'; } // Format the response with markdown-like formatting. - const formattedResponse = formatAIResponse( response ); - if ( responseEl ) { - responseEl.innerHTML = formattedResponse; + const formattedResponse = this.formatAIResponse( response ); + if ( this.elements.responseEl ) { + this.elements.responseEl.innerHTML = formattedResponse; } // Add cached indicator if applicable. - if ( cached && responseEl ) { + if ( cached && this.elements.responseEl ) { const cachedIndicator = document.createElement( 'p' ); cachedIndicator.className = 'prpl-ai-cached-indicator'; cachedIndicator.style.fontSize = '0.9em'; cachedIndicator.style.color = '#666'; cachedIndicator.style.marginTop = '10px'; cachedIndicator.textContent = '(Cached result)'; - responseEl.appendChild( cachedIndicator ); + this.elements.responseEl.appendChild( cachedIndicator ); } - }; + } /** * Show error state. * * @param {string} message - The error message. */ - const showError = ( message ) => { - if ( instructionsEl ) { - instructionsEl.style.display = 'none'; + showError( message ) { + if ( this.elements.instructionsEl ) { + this.elements.instructionsEl.style.display = 'none'; } - if ( loadingEl ) { - loadingEl.style.display = 'none'; + if ( this.elements.loadingEl ) { + this.elements.loadingEl.style.display = 'none'; } - if ( executeButton ) { - executeButton.style.display = 'none'; + if ( this.elements.executeButton ) { + this.elements.executeButton.style.display = 'none'; } - if ( retryButton ) { - retryButton.style.display = 'inline-block'; + if ( this.elements.retryButton ) { + this.elements.retryButton.style.display = 'inline-block'; } - if ( completeButton ) { - completeButton.style.display = 'none'; + if ( this.elements.completeButton ) { + this.elements.completeButton.style.display = 'none'; } - if ( resultEl ) { - resultEl.style.display = 'none'; + if ( this.elements.resultEl ) { + this.elements.resultEl.style.display = 'none'; } - if ( errorEl ) { - errorEl.style.display = 'block'; + if ( this.elements.errorEl ) { + this.elements.errorEl.style.display = 'block'; } - if ( errorMessageEl ) { - errorMessageEl.textContent = message; + if ( this.elements.errorMessageEl ) { + this.elements.errorMessageEl.textContent = message; } - }; + } /** * Format AI response with basic HTML formatting. @@ -145,7 +313,7 @@ * @param {string} text - The raw AI response text. * @return {string} Formatted HTML string. */ - const formatAIResponse = ( text ) => { + formatAIResponse( text ) { if ( ! text ) { return ''; } @@ -162,20 +330,20 @@ } ); return paragraphs.join( '' ); - }; + } /** * Execute the AI task. * * @param {number} taskId - The server task ID. */ - const executeTask = ( taskId ) => { + executeTask( taskId ) { if ( ! taskId ) { - showError( 'Invalid task ID.' ); + this.showError( 'Invalid task ID.' ); return; } - showLoading(); + this.showLoading(); // Make AJAX request to execute the AI task. progressPlannerAjaxRequest( { @@ -191,7 +359,7 @@ const errorMessage = response.data?.message || 'Failed to execute AI task. Please try again.'; - showError( errorMessage ); + this.showError( errorMessage ); return; } @@ -199,52 +367,35 @@ const aiResponse = data.ai_response || ''; const cached = data.cached || false; - showResult( aiResponse, cached ); + this.showResult( aiResponse, cached ); } ) .catch( ( error ) => { console.error( 'AI task execution error:', error ); - showError( + this.showError( 'An error occurred while analyzing your site. Please try again.' ); } ); - }; - - // Execute button click handler - get task ID from web component attribute. - if ( executeButton ) { - executeButton.addEventListener( 'click', () => { - const taskId = - webComponent?.getAttribute( 'data-task-id' ) || - executeButton.dataset.taskId; - console.log( 'Execute button clicked, task ID:', taskId ); - if ( taskId ) { - executeTask( parseInt( taskId ) ); - } else { - console.error( 'No task ID found' ); - showError( 'Task ID not found. Please try again.' ); - } - } ); } - // Retry button click handler. - if ( retryButton ) { - retryButton.addEventListener( 'click', () => { - const taskId = - webComponent?.getAttribute( 'data-task-id' ) || - retryButton.dataset.taskId; - if ( taskId ) { - executeTask( parseInt( taskId ) ); - } else { - console.error( 'No task ID found' ); - showError( 'Task ID not found. Please try again.' ); - } - } ); - } - }; + /** + * Decodes HTML entities in a string (like ", &, etc.) + * @param {string} str The string to decode. + * @return {string} The decoded string. + */ + decodeHtmlEntities( str ) { + if ( typeof str !== 'string' ) { + return str; + } - // Initialize when DOM is ready. - if ( document.readyState === 'loading' ) { - document.addEventListener( 'DOMContentLoaded', handleAITaskExecution ); - } else { - handleAITaskExecution(); + return str + .replace( /"/g, '"' ) + .replace( /'/g, "'" ) + .replace( /</g, '<' ) + .replace( />/g, '>' ) + .replace( /&/g, '&' ); + } } + + // Initialize the component. + new AITask(); } )(); diff --git a/assets/js/web-components/prpl-ai-task-popover.js b/assets/js/web-components/prpl-ai-task-popover.js index d7a49b1a6..79d11d3ef 100644 --- a/assets/js/web-components/prpl-ai-task-popover.js +++ b/assets/js/web-components/prpl-ai-task-popover.js @@ -8,160 +8,19 @@ customElements.define( 'prpl-ai-task-popover', class extends PrplInteractiveTask { - constructor() { - super(); - this.listenersAttached = false; - } - - /** - * Runs when the popover is opening. - * Use this to set up task-specific data. - */ - popoverOpening() { - super.popoverOpening(); - - console.log( 'AI Task: Popover opening' ); - - // Get task data from the trigger button - const popoverId = this.getAttribute( 'popover-id' ); - const popover = document.getElementById( popoverId ); - - // Find the trigger button that was just clicked - // When a button with popovertarget is clicked, it becomes the activeElement - let triggerButton = document.activeElement; - - // Verify it's the correct button (has the matching popovertarget) - if ( ! triggerButton || triggerButton.getAttribute( 'popovertarget' ) !== popoverId ) { - // Fallback: search for the button within the closest task element if we can find one - console.warn( 'AI Task: Could not determine which trigger button was clicked via activeElement' ); - triggerButton = document.querySelector( - `button[popovertarget="${ popoverId }"]` - ); - } - - if ( ! triggerButton ) { - console.warn( 'AI Task: Could not find trigger button' ); - return; - } - - console.log( 'AI Task: Found trigger button:', triggerButton ); - - // Get task data from button attributes - const taskId = triggerButton.dataset.taskId; - const taskPrompt = triggerButton.dataset.taskPrompt; - - // Get the task element and its slug for completion - const taskElement = triggerButton.closest( '.prpl-suggested-task' ); - const taskSlug = taskElement ? taskElement.dataset.taskId : null; - - console.log( 'AI Task: Task ID:', taskId ); - console.log( 'AI Task: Task slug:', taskSlug ); - console.log( 'AI Task: Task prompt:', taskPrompt ); - - // Store task data on web component - this.setAttribute( 'data-task-id', taskId ); - this.setAttribute( 'current-task-id', taskSlug ); - - // Display task prompt if available - const promptEl = popover.querySelector( '.prpl-ai-task-prompt' ); - const promptTextEl = popover.querySelector( - '.prpl-ai-task-prompt-text' - ); - if ( taskPrompt && promptTextEl ) { - promptTextEl.textContent = taskPrompt; - promptEl.style.display = 'block'; - } else if ( promptEl ) { - promptEl.style.display = 'none'; - } - - // Reset popover state - const instructionsEl = popover.querySelector( - '.prpl-ai-task-instructions' - ); - const loadingEl = popover.querySelector( '.prpl-ai-task-loading' ); - const resultEl = popover.querySelector( '.prpl-ai-task-result' ); - const errorEl = popover.querySelector( '.prpl-ai-task-error' ); - const executeButton = popover.querySelector( - '.prpl-ai-task-execute' - ); - const retryButton = popover.querySelector( '.prpl-ai-task-retry' ); - const completeButton = popover.querySelector( - '.prpl-ai-task-complete' - ); - - if ( instructionsEl ) { - instructionsEl.style.display = 'block'; - } - if ( loadingEl ) { - loadingEl.style.display = 'none'; - } - if ( resultEl ) { - resultEl.style.display = 'none'; - } - if ( errorEl ) { - errorEl.style.display = 'none'; - } - if ( executeButton ) { - executeButton.style.display = 'inline-block'; - executeButton.dataset.taskId = taskId; - } - if ( retryButton ) { - retryButton.style.display = 'none'; - retryButton.dataset.taskId = taskId; - } - if ( completeButton ) { - completeButton.style.display = 'none'; - } - } - - /** - * Attach button event listeners. - * Overrides parent to prevent duplicate listeners. - */ - attachDefaultEventListeners() { - // Prevent attaching listeners multiple times - if ( this.listenersAttached ) { - console.log( - 'AI Task: Event listeners already attached, skipping' - ); - return; - } - - console.log( 'AI Task: Attaching event listeners' ); - - // Add event listeners. - this.querySelectorAll( 'button' ).forEach( ( buttonElement ) => { - buttonElement.addEventListener( 'click', ( e ) => { - const button = e.target.closest( 'button' ); - const action = button?.dataset.action; - if ( action && typeof this[ action ] === 'function' ) { - this[ action ](); - } - } ); - } ); - - this.listenersAttached = true; - } - /** * Complete the task. * Overrides parent to use current-task-id attribute instead of provider-id. */ completeTask() { - console.log( '=== AI Task completeTask called ===' ); - console.trace( 'Stack trace' ); - - // Prevent multiple completions + // Prevent multiple completions. if ( this.isCompleting ) { - console.warn( - 'AI Task: Already completing, ignoring duplicate call' - ); return; } this.isCompleting = true; - // Get the current task ID that was set when the popover opened + // Get the current task ID that was set when the popover opened. const currentTaskId = this.getAttribute( 'current-task-id' ); if ( ! currentTaskId ) { @@ -170,28 +29,14 @@ customElements.define( return; } - console.log( 'AI Task: Completing task with ID:', currentTaskId ); - const tasks = document.querySelectorAll( '#prpl-suggested-tasks-list .prpl-suggested-task' ); - console.log( - 'AI Task: Found', - tasks.length, - 'task elements in list' - ); - let foundMatch = false; tasks.forEach( ( taskElement ) => { - console.log( - 'AI Task: Checking task element with ID:', - taskElement.dataset.taskId - ); if ( taskElement.dataset.taskId === currentTaskId ) { - console.log( 'AI Task: Found matching task element' ); - if ( foundMatch ) { console.warn( 'AI Task: Found duplicate matching task element! This should not happen.' @@ -211,13 +56,9 @@ customElements.define( const postId = parseInt( taskElement.dataset.postId ); if ( postId ) { - console.log( - 'AI Task: Calling maybeComplete with post ID:', - postId - ); prplSuggestedTask.maybeComplete( postId ); - // Reset flag after a delay + // Reset flag after a delay. setTimeout( () => { this.isCompleting = false; }, 1000 ); diff --git a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php index 5d89b496f..59e990438 100644 --- a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php +++ b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php @@ -363,17 +363,35 @@ public function enqueue_scripts( $hook ) { */ public function add_task_actions( $data = [], $actions = [] ) { - $task_id = $data['meta']['prpl_ai_task_server_id'] ?? ''; - $task_prompt = $data['meta']['prpl_ai_prompt_template'] ?? ''; + $remote_task_id = $data['meta']['prpl_ai_task_server_id'] ?? ''; + $task_prompt = $data['meta']['prpl_ai_prompt_template'] ?? ''; - if ( ! $task_id ) { + if ( ! $remote_task_id ) { return $actions; } // Add the "Analyze" button that opens the popover. $actions[] = [ 'priority' => 10, - 'html' => '', + 'html' => \sprintf( + ' + %s + ', + \htmlspecialchars( + \wp_json_encode( + [ + 'remote_task_id' => $remote_task_id, + 'task_prompt' => $task_prompt, + ] + ), + ENT_QUOTES, + 'UTF-8' + ), + \esc_attr( static::POPOVER_ID ), + \esc_html__( 'Analyze', 'progress-planner' ) + ), ]; return $actions; From 0ed23411a5c2b7e312f4f016e5e24e6da86df9fd Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Tue, 4 Nov 2025 12:08:58 +0100 Subject: [PATCH 8/8] phpcs --- classes/class-suggested-tasks.php | 10 +++--- .../providers/class-ai-tasks-from-server.php | 34 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/classes/class-suggested-tasks.php b/classes/class-suggested-tasks.php index 938faf29d..51603b3dc 100644 --- a/classes/class-suggested-tasks.php +++ b/classes/class-suggested-tasks.php @@ -375,29 +375,29 @@ public function register_post_type() { ); $rest_meta_fields = [ - 'prpl_url' => [ + 'prpl_url' => [ 'type' => 'string', 'single' => true, 'show_in_rest' => true, ], - 'menu_order' => [ + 'menu_order' => [ 'type' => 'number', 'single' => true, 'show_in_rest' => true, 'default' => 0, ], - 'prpl_is_ai_task' => [ + 'prpl_is_ai_task' => [ 'type' => 'boolean', 'single' => true, 'show_in_rest' => true, 'default' => false, ], - 'prpl_ai_task_server_id' => [ + 'prpl_ai_task_server_id' => [ 'type' => 'number', 'single' => true, 'show_in_rest' => true, ], - 'prpl_ai_prompt_template' => [ + 'prpl_ai_prompt_template' => [ 'type' => 'string', 'single' => true, 'show_in_rest' => true, diff --git a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php index 59e990438..3179ae39a 100644 --- a/classes/suggested-tasks/providers/class-ai-tasks-from-server.php +++ b/classes/suggested-tasks/providers/class-ai-tasks-from-server.php @@ -116,23 +116,23 @@ public function get_tasks_to_inject() { // Create task data. $task_data = [ - 'task_id' => $task_id, - 'provider_id' => $this->get_provider_id(), - 'post_title' => $ai_task['title'] ?? 'AI Task', - 'description' => 'Click "Analyze" to get AI-powered insights for your site.', - 'parent' => 0, - 'priority' => $this->get_priority(), - 'points' => $this->get_points(), - 'date' => \gmdate( 'YW' ), - 'url' => '', - 'url_target' => '_self', - 'link_setting' => [], - 'dismissable' => $this->is_dismissable(), - 'external_link_url' => '', - 'popover_id' => 'prpl-popover-' . static::POPOVER_ID, - 'is_ai_task' => true, - 'ai_task_server_id' => $ai_task['task_id'], - 'ai_prompt_template' => $ai_task['ai_prompt_template'] ?? '', + 'task_id' => $task_id, + 'provider_id' => $this->get_provider_id(), + 'post_title' => $ai_task['title'] ?? 'AI Task', + 'description' => 'Click "Analyze" to get AI-powered insights for your site.', + 'parent' => 0, + 'priority' => $this->get_priority(), + 'points' => $this->get_points(), + 'date' => \gmdate( 'YW' ), + 'url' => '', + 'url_target' => '_self', + 'link_setting' => [], + 'dismissable' => $this->is_dismissable(), + 'external_link_url' => '', + 'popover_id' => 'prpl-popover-' . static::POPOVER_ID, + 'is_ai_task' => true, + 'ai_task_server_id' => $ai_task['task_id'], + 'ai_prompt_template' => $ai_task['ai_prompt_template'] ?? '', ]; $tasks_to_inject[] = \progress_planner()->get_suggested_tasks_db()->add( $task_data );