| 1 |
<?php |
| 2 |
|
| 3 |
namespace Templately\Modules\FullSiteImport\Concerns; |
| 4 |
|
| 5 |
use Elementor\Plugin; |
| 6 |
use Error; |
| 7 |
use Exception; |
| 8 |
use Templately\API\API; |
| 9 |
use Templately\Modules\FullSiteImport\Exception\FatalErrorException; |
| 10 |
use Templately\Modules\FullSiteImport\Exception\RetryableErrorException; |
| 11 |
use Templately\Modules\FullSiteImport\Exception\UnknownErrorException; |
| 12 |
use Templately\Modules\FullSiteImport\Utils\LogHandler; |
| 13 |
use Templately\Modules\FullSiteImport\Utils\Utils; |
| 14 |
use Templately\Modules\FullSiteImport\Utils\SessionData; |
| 15 |
use Templately\Modules\FullSiteImport\Utils\AIUtils; |
| 16 |
use Templately\Utils\Helper; |
| 17 |
use Templately\Utils\Options; |
| 18 |
use Templately\Utils\Response\AjaxResponder; |
| 19 |
use Templately\Utils\Response\ErrorCode; |
| 20 |
|
| 21 |
/** |
| 22 |
* HandlesAiEndpoints — extracted verbatim from FullSiteImport (behavior-preserving). |
| 23 |
* Composed back into FullSiteImport via `use`; $this and method resolution unchanged. |
| 24 |
*/ |
| 25 |
trait HandlesAiEndpoints { |
| 26 |
/** |
| 27 |
* Frozen AJAX action `templately_pack_ai_get_json` — a thin alias to the |
| 28 |
* single retrieval behavior (D7). Kept so the action name contract is stable. |
| 29 |
*/ |
| 30 |
public function ai_get_json() { |
| 31 |
$this->retrieve_ai_content(); |
| 32 |
} |
| 33 |
|
| 34 |
/** |
| 35 |
* The ONE content-retrieval behavior (FR-002/D7): read whatever AI content is |
| 36 |
* on disk for the process and respond with the standard envelope (FR-003). |
| 37 |
* Both `ai_get_json` (read) and `ai_poll_template` (poll-then-read) flow |
| 38 |
* through here. Responses are ADDITIVE — the existing `process_id`/`templates`/ |
| 39 |
* `is_last_part` payload is preserved, with `{success,terminal,code,message}` |
| 40 |
* merged in (envelope sent INSIDE wp_send_json's data, where the client's poll |
| 41 |
* loop already reads `terminal` from the error path). |
| 42 |
*/ |
| 43 |
private function retrieve_ai_content() { |
| 44 |
// read json data from post body |
| 45 |
$body = file_get_contents('php://input'); |
| 46 |
$data = json_decode($body, true); |
| 47 |
|
| 48 |
if(empty($data['ai_page_ids'])){ |
| 49 |
AjaxResponder::error(ErrorCode::AI_INVALID_PROCESS, 'Invalid ai_page_ids'); |
| 50 |
return; |
| 51 |
} |
| 52 |
|
| 53 |
if(!isset($_GET['session_id'])){ |
| 54 |
AjaxResponder::error(ErrorCode::AI_INVALID_PROCESS, 'Invalid session_id'); |
| 55 |
return; |
| 56 |
} |
| 57 |
|
| 58 |
$session_id = isset($_GET['session_id']) ? sanitize_text_field($_GET['session_id']) : null; |
| 59 |
$process_id = $data['process_id'] ?? null; |
| 60 |
$ai_page_ids = $data['ai_page_ids'] ?? null; |
| 61 |
|
| 62 |
$this->request_params = $this->get_session_data(); |
| 63 |
try { |
| 64 |
$this->manifest = $this->read_manifest($this->request_params['dir_path']); |
| 65 |
} catch (\Exception $th) { |
| 66 |
AjaxResponder::error(ErrorCode::AI_INTERNAL_ERROR, $th->getMessage()); |
| 67 |
} |
| 68 |
|
| 69 |
if(!empty($session_id) && empty($process_id)){ |
| 70 |
if ( !empty($this->request_params['process_id']) ){ |
| 71 |
$process_id = $this->request_params['process_id'] ?? null; |
| 72 |
} else { |
| 73 |
$process_id = AIUtils::get_ai_process_id_by_session_id($session_id); |
| 74 |
} |
| 75 |
} |
| 76 |
|
| 77 |
if(empty($process_id)){ |
| 78 |
AjaxResponder::error(ErrorCode::AI_INVALID_PROCESS, 'Invalid process_id'); |
| 79 |
return; |
| 80 |
} |
| 81 |
|
| 82 |
$process_data = AIUtils::get_ai_process_data_by_process_id($process_id); |
| 83 |
if (!empty($process_data['preview_error'])) { |
| 84 |
// Terminal remote failure persisted on the process row. |
| 85 |
AjaxResponder::error(ErrorCode::AI_REMOTE_FAILED, $process_data['preview_error']); |
| 86 |
} |
| 87 |
|
| 88 |
// Use the new common function to read AI template data directly |
| 89 |
$result = AIUtils::read_ai_template_data($session_id, $ai_page_ids, $this->request_params['dir_path']); |
| 90 |
|
| 91 |
// Check if this is called from polling endpoint and include additional data |
| 92 |
$response_data = ['process_id' => $process_id, 'templates' => $result]; |
| 93 |
|
| 94 |
$is_last_part = null; |
| 95 |
if (isset($this->polling_is_last_part)) { |
| 96 |
$is_last_part = $this->polling_is_last_part; |
| 97 |
$response_data['is_last_part'] = $is_last_part; |
| 98 |
|
| 99 |
// Clean up the polling property |
| 100 |
unset($this->polling_is_last_part); |
| 101 |
} |
| 102 |
|
| 103 |
// Code reflects completeness: all pages on disk (or remote says last part) |
| 104 |
// → ok (terminal); otherwise not_ready (the client keeps polling). |
| 105 |
$total_pages = 0; |
| 106 |
if (is_array($ai_page_ids)) { |
| 107 |
foreach ($ai_page_ids as $ids) { |
| 108 |
$total_pages += is_array($ids) ? count($ids) : 0; |
| 109 |
} |
| 110 |
} |
| 111 |
$ready = ! empty($is_last_part) || ($total_pages > 0 && count($result) >= $total_pages); |
| 112 |
|
| 113 |
// The one place the whole generation's progress is actually countable. Logged on |
| 114 |
// every poll at debug, and raised to a warning for the state that strands the |
| 115 |
// client: the remote says this was the last part, yet pages are still unwritten. |
| 116 |
// The client turns exactly that into "N pages still missing when process marked as |
| 117 |
// complete" and stops — so without this line the only record of WHY is a message in |
| 118 |
// the browser. |
| 119 |
$on_disk = is_array($result) ? count($result) : 0; |
| 120 |
$stranded = ! empty($is_last_part) && $total_pages > 0 && $on_disk < $total_pages; |
| 121 |
Helper::log( |
| 122 |
sprintf( |
| 123 |
'retrieve_ai_content: %s — %d/%d page(s) on disk, is_last_part=%s%s', |
| 124 |
$process_id, |
| 125 |
$on_disk, |
| 126 |
$total_pages, |
| 127 |
null === $is_last_part ? '(not polled)' : ( $is_last_part ? 'yes' : 'no' ), |
| 128 |
$stranded ? ' — STRANDED: marked complete with pages missing' : '' |
| 129 |
), |
| 130 |
'retrieve_ai_content', |
| 131 |
$stranded ? 'warning' : 'debug' |
| 132 |
); |
| 133 |
|
| 134 |
AjaxResponder::success(API::ai_envelope($ready ? 'ok' : 'not_ready', '', $response_data)); |
| 135 |
} |
| 136 |
|
| 137 |
/** |
| 138 |
* AJAX handler for polling AI template generation status on local sites |
| 139 |
* Makes GET request to API endpoint and returns data in same format as ai_get_json() |
| 140 |
*/ |
| 141 |
public function ai_poll_template() { |
| 142 |
// Read JSON data from post body |
| 143 |
$body = file_get_contents('php://input'); |
| 144 |
$data = json_decode($body, true); |
| 145 |
|
| 146 |
$process_id = $data['process_id'] ?? null; |
| 147 |
$ai_page_ids = $data['ai_page_ids'] ?? null; |
| 148 |
|
| 149 |
if(empty($process_id)){ |
| 150 |
AjaxResponder::error(ErrorCode::AI_INVALID_PROCESS, 'Invalid process_id'); |
| 151 |
return; |
| 152 |
} |
| 153 |
|
| 154 |
// Validate and get AI process data using centralized method |
| 155 |
$process_data = AIUtils::validate_and_get_process_data($process_id); |
| 156 |
if (is_wp_error($process_data)) { |
| 157 |
$this->retrieve_ai_content(); |
| 158 |
return; |
| 159 |
} |
| 160 |
|
| 161 |
$session_id = $process_data['session_id']; |
| 162 |
$ai_page_ids = $process_data['ai_page_ids']; |
| 163 |
|
| 164 |
// Use the common polling function to handle all template processing |
| 165 |
$polling_result = AIUtils::poll_for_template($process_id, $session_id, $ai_page_ids); |
| 166 |
|
| 167 |
// Terminal remote error (401/403/404): stop polling and report a real error. |
| 168 |
// Still HTTP 200 (AjaxResponder always is), but the body is the standard |
| 169 |
// error envelope now, so it REJECTS on the client instead of resolving with |
| 170 |
// `success:false`. The poll loop stops on the error's `retryable:false` |
| 171 |
// rather than on a bespoke `terminal` flag — see pollAIContent's |
| 172 |
// onAttemptError. FR-003. |
| 173 |
if (is_wp_error($polling_result)) { |
| 174 |
$error_data = $polling_result->get_error_data(); |
| 175 |
AjaxResponder::error(ErrorCode::AI_REMOTE_FAILED, $polling_result->get_error_message(), [ |
| 176 |
'status' => isset($error_data['status']) ? $error_data['status'] : null, |
| 177 |
'context' => [ 'process_id' => $process_id ], |
| 178 |
]); |
| 179 |
return; |
| 180 |
} |
| 181 |
|
| 182 |
if (!$polling_result) { |
| 183 |
// Transient miss — fall back to the read path (keep waiting). |
| 184 |
$this->retrieve_ai_content(); |
| 185 |
return; |
| 186 |
} |
| 187 |
|
| 188 |
// After polling and processing templates, read + respond via the single |
| 189 |
// retrieval behavior (reuses all the existing logic without duplication). |
| 190 |
$this->retrieve_ai_content(); |
| 191 |
} |
| 192 |
|
| 193 |
|
| 194 |
} |
| 195 |
|