PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.0
MxChat – AI Chatbot & Content Generation for WordPress v3.2.0
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | includes/class-mxchat-content-generator.php +35 -270 3.2.213.2.0 View file →
@@ -511,14 +511,8 @@
511 511 if (!empty($image_ids)) {
512 512 update_post_meta($post_id, '_mxchat_image_ids', $image_ids);
513 513 }
514 514
515 - // Sync the inline `<img alt="...">` text from the generated body HTML
516 - // into each attachment's _wp_attachment_image_alt postmeta so RankMath,
517 - // schema markup, and the media library all see the alt text — not just
518 - // the inline body HTML. (plan-mxchat-20260510-2be2da)
519 - $this->sync_image_alts_from_content($full_content, $image_urls);
520 -
521 515 // Apply fullwidth/title settings via theme-specific post meta
522 516 $this->apply_layout_settings($post_id, $layout, $title_display);
523 517
524 518 // Step 5: Fill SEO metadata
@@ -731,21 +725,10 @@
731 725 */
732 726 private function plan_content($prompt, $content_type) {
733 727 $type_label = ($content_type === 'page') ? 'landing page' : 'blog post';
734 728
735 - // "Images per article" control (1–5, default 3). Drives how many sections the
736 - // planner marks needs_image:true. When image generation is off, force zero.
737 - $plan_options = get_option('mxchat_options', array());
738 - $images_enabled = ($plan_options['content_enable_images'] ?? 'on') === 'on';
739 - $image_count = max(1, min(5, (int) ($plan_options['content_image_count'] ?? 3)));
740 - if ($images_enabled) {
741 - $image_instruction = "- Select EXACTLY {$image_count} section(s) to illustrate: set \"needs_image\": true with a detailed \"image_prompt\" on exactly those {$image_count} section(s) and \"needs_image\": false with an empty \"image_prompt\" on every other section. Spread the illustrated sections across the article (favor the hero and the most visual content sections)";
742 - } else {
743 - $image_instruction = "- Set \"needs_image\": false and \"image_prompt\": \"\" on ALL sections (image generation is turned off for this site)";
744 - }
729 + $system_prompt = "You are a professional content strategist. The user wants to create a {$type_label}. Analyze their request and create a detailed content plan.\n\nYou MUST respond with ONLY valid JSON (no markdown, no code fences, no commentary). Use this exact structure:\n\n{\"title\": \"SEO-optimized title\", \"slug\": \"url-friendly-slug\", \"meta_description\": \"155 character meta description for SEO\", \"keywords\": [\"keyword1\", \"keyword2\", \"keyword3\", \"keyword4\", \"keyword5\"], \"links\": [{\"label\": \"Button or link text\", \"url\": \"https://example.com/page\"}], \"sections\": [{\"type\": \"hero\", \"heading\": \"Main heading\", \"subheading\": \"Supporting text\", \"needs_image\": true, \"image_prompt\": \"Detailed prompt for hero image\"}, {\"type\": \"content\", \"heading\": \"Section heading\", \"key_points\": [\"point 1\", \"point 2\"], \"needs_image\": true, \"image_prompt\": \"Detailed prompt for section image\"}, {\"type\": \"content\", \"heading\": \"Another section\", \"key_points\": [\"point 1\", \"point 2\"], \"needs_image\": false, \"image_prompt\": \"\"}, {\"type\": \"cta\", \"heading\": \"Call to action heading\", \"subheading\": \"CTA supporting text\", \"needs_image\": false, \"image_prompt\": \"\"}]}\n\nGuidelines:\n- Create 5-8 sections for blog posts, 4-6 for landing pages\n- Include 2-4 sections that need images\n- Image prompts should be detailed, descriptive, and suitable for AI image generation\n- Image prompts should describe photorealistic or illustrative images relevant to the content\n- Keywords should be relevant long-tail SEO keywords\n- Title should be compelling and SEO-friendly\n- IMPORTANT: If the user specifies any URLs or links (for buttons, CTAs, navigation, etc.), you MUST capture every one of them in the \"links\" array with its label and exact URL. If no URLs are mentioned, use an empty array [].";
745 730
746 - $system_prompt = "You are a professional content strategist. The user wants to create a {$type_label}. Analyze their request and create a detailed content plan.\n\nYou MUST respond with ONLY valid JSON (no markdown, no code fences, no commentary). Use this exact structure:\n\n{\"title\": \"SEO-optimized title\", \"slug\": \"url-friendly-slug\", \"meta_description\": \"155 character meta description for SEO\", \"keywords\": [\"keyword1\", \"keyword2\", \"keyword3\", \"keyword4\", \"keyword5\"], \"links\": [{\"label\": \"Button or link text\", \"url\": \"https://example.com/page\"}], \"sections\": [{\"type\": \"hero\", \"heading\": \"Main heading\", \"subheading\": \"Supporting text\", \"needs_image\": true, \"image_prompt\": \"Detailed prompt for hero image\"}, {\"type\": \"content\", \"heading\": \"Section heading\", \"key_points\": [\"point 1\", \"point 2\"], \"needs_image\": true, \"image_prompt\": \"Detailed prompt for section image\"}, {\"type\": \"content\", \"heading\": \"Another section\", \"key_points\": [\"point 1\", \"point 2\"], \"needs_image\": false, \"image_prompt\": \"\"}, {\"type\": \"cta\", \"heading\": \"Call to action heading\", \"subheading\": \"CTA supporting text\", \"needs_image\": false, \"image_prompt\": \"\"}]}\n\nGuidelines:\n- Create 5-9 sections for blog posts, 7-11 for landing pages — add more if the user's request warrants extensive coverage\n- For landing pages, draw from sections like: hero, problem/solution, features, benefits, how-it-works, use cases, testimonials/social proof, pricing or comparison, FAQ, and final CTA — pick whichever fit the request\n- Each content section should include 3-5 substantive key_points (full descriptive sentences, not one-word labels) so the HTML generator has enough material to write meaningful copy\n{$image_instruction}\n- Image prompts should be detailed, descriptive, and suitable for AI image generation\n- Image prompts should describe photorealistic or illustrative images relevant to the content\n- Keywords should be relevant long-tail SEO keywords\n- Title should be compelling and SEO-friendly\n- IMPORTANT: If the user specifies any URLs or links (for buttons, CTAs, navigation, etc.), you MUST capture every one of them in the \"links\" array with its label and exact URL. If no URLs are mentioned, use an empty array [].";
747 -
748 731 $messages = array(
749 732 array('role' => 'user', 'content' => "Create a {$type_label} about: {$prompt}"),
750 733 );
751 734
@@ -799,15 +782,8 @@
799 782 );
800 783 }
801 784 }
802 785
803 - // Safety cap: never generate more than the user-configured "Images per article"
804 - // count (1–5, default 3), even if the planner over-marked needs_image sections.
805 - $image_count = max(1, min(5, (int) ($options['content_image_count'] ?? 3)));
806 - if (count($image_sections) > $image_count) {
807 - $image_sections = array_slice($image_sections, 0, $image_count);
808 - }
809 -
810 786 if (empty($image_sections)) {
811 787 return array();
812 788 }
813 789
@@ -996,9 +972,9 @@
996 972 'model' => 'gpt-image-1.5',
997 973 'prompt' => $prompt,
998 974 'n' => 1,
999 975 'size' => '1536x1024',
1000 - 'quality' => $options['content_image_quality'] ?? 'auto',
976 + 'quality' => 'medium',
1001 977 'output_format' => 'png',
1002 978 )),
1003 979 );
1004 980 }
@@ -1120,9 +1096,9 @@
1120 1096 'model' => 'gpt-image-1.5',
1121 1097 'prompt' => $prompt,
1122 1098 'n' => 1,
1123 1099 'size' => '1536x1024',
1124 - 'quality' => $options['content_image_quality'] ?? 'auto',
1100 + 'quality' => 'medium',
1125 1101 'output_format' => 'png',
1126 1102 );
1127 1103
1128 1104 $response = wp_remote_post('https://api.openai.com/v1/images/generations', array(
@@ -1614,10 +1590,8 @@
1614 1590 - Alternate light (#ffffff) and subtle gray (#f8fafc) backgrounds
1615 1591 - Two-column layouts alternating content left/right
1616 1592 - Card grids for features/benefits
1617 1593 - Each section gets its own mxg- class for unique styling
1618 -- Write substantive copy in every section — expand each plan key_point into a full sentence or short paragraph (roughly 80-180 words per content section). Do NOT condense the plan into one-line bullets
1619 -- Vary the structure across sections: paragraphs, feature card grids, numbered steps, stat callouts, testimonial quotes, comparison tables, or FAQ blocks where they fit the content
1620 1594
1621 1595 TYPOGRAPHY:
1622 1596 - Hero heading: 3rem desktop, scales down in your media queries
1623 1597 - Section headings: 2.25rem desktop
@@ -1932,25 +1906,8 @@
1932 1906 padding: 0 !important;
1933 1907 max-width: 100% !important;
1934 1908 width: 100% !important;
1935 1909 }
1936 -/* WordPress Block Themes (Twenty Twenty-Two → Twenty Twenty-Five)
1937 - These use theme.json layout constraints rather than classic .entry-content,
1938 - so the selectors above do not apply — neutralize them here. */
1939 -.wp-site-blocks,
1940 -.wp-block-post-content,
1941 -.wp-block-group.has-global-padding,
1942 -.has-global-padding {
1943 - padding-left: 0 !important;
1944 - padding-right: 0 !important;
1945 - max-width: 100% !important;
1946 -}
1947 -.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)),
1948 -.wp-block-post-content > :where(:not(.alignleft):not(.alignright):not(.alignfull)) {
1949 - max-width: 100% !important;
1950 - margin-left: 0 !important;
1951 - margin-right: 0 !important;
1952 -}
1953 1910 ';
1954 1911 }
1955 1912
1956 1913 /**
@@ -2166,101 +2123,18 @@
2166 2123 $keywords_string = implode(', ', $keywords);
2167 2124
2168 2125 $this->set_meta_description($post_id, $meta_description);
2169 2126 $this->set_focus_keyword($post_id, !empty($focus_keyword) ? $focus_keyword : $keywords_string);
2170 - $this->set_seo_title($post_id, $plan['title'] ?? '');
2171 2127 }
2172 2128
2173 - /**
2174 - * Write the SEO title to the active SEO plugin's title postmeta so
2175 - * RankMath / Yoast / AIOSEO don't fall back to the bare post_title.
2176 - * (plan-mxchat-20260510-b89332)
2177 - *
2178 - * v1: use the generated post_title as the SEO title. A future plan may
2179 - * add a separately-generated 50-58 char SEO-optimized title.
2180 - */
2181 - private function set_seo_title($post_id, $title) {
2182 - $title = sanitize_text_field($title);
2183 - if (empty($title)) {
2184 - return;
2185 - }
2186 - $plugin = $this->get_seo_plugin();
2187 - $keys = array(
2188 - 'rankmath' => 'rank_math_title',
2189 - 'yoast' => '_yoast_wpseo_title',
2190 - 'aioseo' => '_aioseo_title',
2191 - );
2192 - if (isset($keys[$plugin])) {
2193 - update_post_meta($post_id, $keys[$plugin], $title);
2194 - }
2195 - }
2196 -
2197 - /**
2198 - * Walk the rendered body HTML and copy the inline `<img alt="...">` text
2199 - * for each generated image into its attachment's _wp_attachment_image_alt
2200 - * postmeta. Matches inline images to attachments by URL.
2201 - * (plan-mxchat-20260510-2be2da)
2202 - */
2203 - private function sync_image_alts_from_content($content, $image_urls) {
2204 - if (empty($content) || empty($image_urls)) {
2205 - return;
2206 - }
2207 -
2208 - // Build a url => attachment_id lookup for fast matching.
2209 - $by_url = array();
2210 - foreach ($image_urls as $img) {
2211 - if (!empty($img['url']) && !empty($img['attachment_id'])) {
2212 - $by_url[$img['url']] = (int) $img['attachment_id'];
2213 - }
2214 - }
2215 - if (empty($by_url)) {
2216 - return;
2217 - }
2218 -
2219 - if (!preg_match_all('/<img\b[^>]*>/i', $content, $matches)) {
2220 - return;
2221 - }
2222 -
2223 - foreach ($matches[0] as $img_tag) {
2224 - if (!preg_match('/\bsrc\s*=\s*"([^"]+)"/i', $img_tag, $src_match)) {
2225 - continue;
2226 - }
2227 - $src = html_entity_decode($src_match[1], ENT_QUOTES);
2228 - if (!isset($by_url[$src])) {
2229 - continue;
2230 - }
2231 - if (!preg_match('/\balt\s*=\s*"([^"]*)"/i', $img_tag, $alt_match)) {
2232 - continue;
2233 - }
2234 - $alt = sanitize_text_field(html_entity_decode($alt_match[1], ENT_QUOTES));
2235 - if ($alt !== '') {
2236 - update_post_meta($by_url[$src], '_wp_attachment_image_alt', $alt);
2237 - }
2238 - }
2239 - }
2240 -
2241 2129 // ─── AI Model Caller ────────────────────────────────────────────────
2242 2130
2243 2131 /**
2244 2132 * Call the configured content model
2245 2133 */
2246 - /**
2247 - * Provider slugs (MxChat_Model_Catalog keys) that call_content_model() can
2248 - * dispatch to. MUST mirror the if/elseif chain below — the Content Model
2249 - * picker derives its provider set from this list (plan ccb751), so a
2250 - * provider added to the dispatch without updating this is never offered,
2251 - * and one added here without dispatch support would break generation.
2252 - * OpenRouter and the custom OpenAI-compatible provider are absent on
2253 - * purpose: their pseudo-model ids would fall through to the OpenAI default
2254 - * branch with the wrong key and endpoint.
2255 - */
2256 - public static function supported_content_providers() {
2257 - return array('openai', 'claude', 'gemini', 'xai', 'deepseek');
2258 - }
2259 -
2260 2134 private function call_content_model($system_prompt, $messages, $max_tokens = 4096) {
2261 2135 $options = get_option('mxchat_options', array());
2262 - $model = $options['content_model'] ?? $options['model'] ?? 'gpt-5.6-sol';
2136 + $model = $options['content_model'] ?? $options['model'] ?? 'gpt-5.1-chat-latest';
2263 2137
2264 2138 // Determine provider from model name
2265 2139 if ($this->is_claude_model($model)) {
2266 2140 return $this->call_claude($model, $options['claude_api_key'] ?? '', $system_prompt, $messages, $max_tokens);
@@ -2278,23 +2152,8 @@
2278 2152
2279 2153 /**
2280 2154 * Call OpenAI-compatible API (OpenAI, xAI, DeepSeek)
2281 2155 */
2282 - /**
2283 - * plan-mxchat-20260714-dcb71c: frozen pre-dcb71c 'content' reasoning ladder,
2284 - * used only when the core model catalog is unavailable. Byte-identical to the
2285 - * old inline if/elseif at the call site.
2286 - */
2287 - private function mxchat_cg_reasoning_effort_fallback($model) {
2288 - if (strpos($model, 'gpt-5') !== 0) return null;
2289 - if ($model === 'gpt-5.2' || $model === 'gpt-5.1-chat-latest') return null;
2290 - if ($model === 'gpt-5.1-2025-11-13') return 'low';
2291 - if ($model === 'gpt-5.5') return 'low';
2292 - if ($model === 'gpt-5.4') return 'low';
2293 - if (in_array($model, array('gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna'), true)) return 'low';
2294 - return 'minimal';
2295 - }
2296 -
2297 2156 private function call_openai_compatible($model, $api_key, $endpoint, $system_prompt, $messages, $max_tokens) {
2298 2157 if (empty($api_key)) {
2299 2158 return new WP_Error('no_api_key', __('API key not configured for the selected content model.', 'mxchat'));
2300 2159 }
@@ -2307,17 +2166,11 @@
2307 2166 'content' => $msg['content'] ?? '',
2308 2167 );
2309 2168 }
2310 2169
2311 - // GPT-5.x models require max_completion_tokens and only support temperature=1.
2312 - // Token key, temperature gate, and reasoning_effort are sourced from the
2313 - // core model catalog (plan-dcb71c); frozen fallbacks preserve exact
2314 - // pre-dcb71c behavior if the catalog class is unavailable.
2315 - $is_gpt5 = strpos($model, 'gpt-5') === 0;
2316 - $catalog = class_exists('MxChat_Model_Catalog');
2317 - $token_key = ($catalog && method_exists('MxChat_Model_Catalog', 'openai_token_param'))
2318 - ? MxChat_Model_Catalog::openai_token_param($model)
2319 - : ($is_gpt5 ? 'max_completion_tokens' : 'max_tokens');
2170 + // GPT-5.x models require max_completion_tokens and only support temperature=1
2171 + $is_gpt5 = strpos($model, 'gpt-5') === 0;
2172 + $token_key = $is_gpt5 ? 'max_completion_tokens' : 'max_tokens';
2320 2173
2321 2174 $body = array(
2322 2175 'model' => $model,
2323 2176 'messages' => $formatted,
@@ -2324,30 +2177,25 @@
2324 2177 $token_key => $max_tokens,
2325 2178 'stream' => false,
2326 2179 );
2327 2180
2328 - $temp_ok = ($catalog && method_exists('MxChat_Model_Catalog', 'supports_temperature'))
2329 - ? MxChat_Model_Catalog::supports_temperature($model)
2330 - : !$is_gpt5;
2331 - if ($temp_ok) {
2181 + if (!$is_gpt5) {
2332 2182 $body['temperature'] = 0.7;
2333 2183 }
2334 2184
2335 - $effort = ($catalog && method_exists('MxChat_Model_Catalog', 'reasoning_effort_for'))
2336 - ? MxChat_Model_Catalog::reasoning_effort_for($model, 'content')
2337 - : $this->mxchat_cg_reasoning_effort_fallback($model);
2338 - if ($effort !== null) {
2339 - $body['reasoning_effort'] = $effort;
2185 + // Add reasoning_effort only for GPT-5 models that support it
2186 + // gpt-5.2 and gpt-5.1-chat-latest don't support reasoning_effort parameter
2187 + if ($is_gpt5 && $model !== 'gpt-5.2' && $model !== 'gpt-5.1-chat-latest') {
2188 + // GPT-5.1 uses 'low' instead of 'minimal'
2189 + if ($model === 'gpt-5.1-2025-11-13') {
2190 + $body['reasoning_effort'] = 'low';
2191 + } elseif ($model === 'gpt-5.4') {
2192 + $body['reasoning_effort'] = 'low';
2193 + } else {
2194 + $body['reasoning_effort'] = 'minimal';
2195 + }
2340 2196 }
2341 2197
2342 - // DeepSeek V4 defaults to thinking mode ON (temperature ignored and
2343 - // reasoning eats the token budget); generation wants the legacy
2344 - // deepseek-chat semantics = non-thinking. Endpoint-gated because this
2345 - // helper is shared with OpenAI and xAI.
2346 - if (strpos($endpoint, 'api.deepseek.com') !== false) {
2347 - $body['thinking'] = array('type' => 'disabled');
2348 - }
2349 -
2350 2198 // Scale timeout with token count — large generation calls need more time
2351 2199 $timeout = ($max_tokens > 8000) ? 300 : 120;
2352 2200
2353 2201 $response = wp_remote_post($endpoint, array(
@@ -2365,38 +2213,8 @@
2365 2213
2366 2214 $status_code = wp_remote_retrieve_response_code($response);
2367 2215 $decoded = json_decode(wp_remote_retrieve_body($response), true);
2368 2216
2369 - // plan-25b972 self-heal: supported reasoning_effort VALUES are per-model;
2370 - // a 400 rejecting the value we sent (stale catalog entry / provider
2371 - // drift) is deterministic — strip the param and retry ONCE.
2372 - if ($status_code === 400
2373 - && isset($body['reasoning_effort'])
2374 - && isset($decoded['error']['message'])
2375 - && is_string($decoded['error']['message'])
2376 - && preg_match('/Unsupported value:.*reasoning_effort/i', $decoded['error']['message'])) {
2377 - if (defined('WP_DEBUG') && WP_DEBUG) {
2378 - error_log(sprintf(
2379 - '[MxChat] content-generator: model %s rejected reasoning_effort \'%s\' — retrying once without the param (plan-25b972).',
2380 - $model, $body['reasoning_effort']
2381 - ));
2382 - }
2383 - unset($body['reasoning_effort']);
2384 - $response = wp_remote_post($endpoint, array(
2385 - 'headers' => array(
2386 - 'Authorization' => 'Bearer ' . $api_key,
2387 - 'Content-Type' => 'application/json',
2388 - ),
2389 - 'body' => wp_json_encode($body),
2390 - 'timeout' => $timeout,
2391 - ));
2392 - if (is_wp_error($response)) {
2393 - return $response;
2394 - }
2395 - $status_code = wp_remote_retrieve_response_code($response);
2396 - $decoded = json_decode(wp_remote_retrieve_body($response), true);
2397 - }
2398 -
2399 2217 if ($status_code !== 200) {
2400 2218 $error_msg = $decoded['error']['message'] ?? __('API request failed with status ', 'mxchat') . $status_code;
2401 2219 return new WP_Error('api_error', $error_msg);
2402 2220 }
@@ -2411,12 +2229,8 @@
2411 2229 /**
2412 2230 * Call Claude (Anthropic) API
2413 2231 */
2414 2232 private function call_claude($model, $api_key, $system_prompt, $messages, $max_tokens) {
2415 - // Anthropic retired claude-opus-4-20250514 / claude-sonnet-4-20250514 on 2026-06-15.
2416 - // Read-time rescue: remap a saved dead ID to the current equivalent before the API call.
2417 - if ($model === 'claude-opus-4-20250514') { $model = 'claude-opus-4-8'; }
2418 - elseif ($model === 'claude-sonnet-4-20250514') { $model = 'claude-sonnet-4-6'; }
2419 2233 if (empty($api_key)) {
2420 2234 return new WP_Error('no_api_key', __('Claude API key not configured.', 'mxchat'));
2421 2235 }
2422 2236
@@ -2436,25 +2250,11 @@
2436 2250 'model' => $model,
2437 2251 'max_tokens' => $max_tokens,
2438 2252 'temperature' => 0.7,
2439 2253 'messages' => $formatted,
2440 - // Prompt-cache breakpoint (plan 1ff43b): generation pipelines reuse
2441 - // the same system prompt across consecutive requests within the
2442 - // 5-minute cache window. Silently no-ops below the model minimum.
2443 - 'system' => trim((string) $system_prompt) === '' ? $system_prompt : array(
2444 - array('type' => 'text', 'text' => $system_prompt, 'cache_control' => array('type' => 'ephemeral')),
2445 - ),
2254 + 'system' => $system_prompt,
2446 2255 );
2447 2256
2448 - // Anthropic removed temperature on Opus 4.7+ flagships (400 if sent) —
2449 - // sourced from the core model catalog (plan-dcb71c); frozen list fallback.
2450 - $omit_temp = (class_exists('MxChat_Model_Catalog') && method_exists('MxChat_Model_Catalog', 'supports_temperature'))
2451 - ? !MxChat_Model_Catalog::supports_temperature($model)
2452 - : in_array($model, array('claude-opus-5', 'claude-opus-4-7', 'claude-opus-4-8', 'claude-fable-5', 'claude-sonnet-5'), true);
2453 - if ($omit_temp) {
2454 - unset($body['temperature']);
2455 - }
2456 -
2457 2257 $timeout = ($max_tokens > 8000) ? 300 : 120;
2458 2258
2459 2259 $response = wp_remote_post('https://api.anthropic.com/v1/messages', array(
2460 2260 'headers' => array(
@@ -2477,16 +2277,10 @@
2477 2277 $error_msg = $decoded['error']['message'] ?? __('Claude API error ', 'mxchat') . $status_code;
2478 2278 return new WP_Error('claude_error', $error_msg);
2479 2279 }
2480 2280
2481 - // claude-fable-5 prepends a thinking block to content — take the
2482 - // first TEXT block, not content[0].
2483 - if (isset($decoded['content']) && is_array($decoded['content'])) {
2484 - foreach ($decoded['content'] as $block) {
2485 - if (isset($block['type'], $block['text']) && $block['type'] === 'text') {
2486 - return trim($block['text']);
2487 - }
2488 - }
2281 + if (isset($decoded['content'][0]['text'])) {
2282 + return trim($decoded['content'][0]['text']);
2489 2283 }
2490 2284
2491 2285 return new WP_Error('unexpected_response', __('Unexpected Claude response format.', 'mxchat'));
2492 2286 }
@@ -2495,11 +2289,8 @@
2495 2289 /**
2496 2290 * Call Gemini API
2497 2291 */
2498 2292 private function call_gemini($model, $api_key, $system_prompt, $messages, $max_tokens) {
2499 - if ($model === 'gemini-3-pro-preview') {
2500 - $model = 'gemini-3.1-pro-preview';
2501 - }
2502 2293 if (empty($api_key)) {
2503 2294 return new WP_Error('no_api_key', __('Gemini API key not configured.', 'mxchat'));
2504 2295 }
2505 2296
@@ -2596,9 +2387,9 @@
2596 2387
2597 2388 $field = sanitize_text_field($_POST['field'] ?? '');
2598 2389 $value = sanitize_text_field($_POST['value'] ?? '');
2599 2390
2600 - $allowed_fields = array('content_model', 'content_image_model', 'content_image_quality', 'content_image_count', 'content_enable_images', 'content_use_placeholders', 'content_internal_linking', 'content_tool_use', 'seo_optimize_meta_desc', 'seo_optimize_seo_title', 'seo_optimize_slug', 'seo_optimize_readability', 'seo_optimize_internal_links', 'seo_optimize_img_alt', 'seo_optimize_featured_img');
2391 + $allowed_fields = array('content_model', 'content_image_model', 'content_enable_images', 'content_use_placeholders', 'content_internal_linking', 'content_tool_use', 'seo_optimize_meta_desc', 'seo_optimize_seo_title', 'seo_optimize_slug', 'seo_optimize_readability', 'seo_optimize_internal_links', 'seo_optimize_img_alt', 'seo_optimize_featured_img');
2601 2392 if (!in_array($field, $allowed_fields, true)) {
2602 2393 wp_send_json_error(array('message' => __('Invalid field.', 'mxchat')));
2603 2394 }
2604 2395
@@ -2606,18 +2397,8 @@
2606 2397 if (in_array($field, array('content_enable_images', 'content_use_placeholders', 'content_internal_linking', 'content_tool_use', 'seo_optimize_meta_desc', 'seo_optimize_seo_title', 'seo_optimize_slug', 'seo_optimize_readability', 'seo_optimize_internal_links', 'seo_optimize_img_alt', 'seo_optimize_featured_img'), true)) {
2607 2398 $value = ($value === 'on') ? 'on' : 'off';
2608 2399 }
2609 2400
2610 - // Enum field: image quality
2611 - if ($field === 'content_image_quality') {
2612 - $value = in_array($value, array('auto', 'low', 'medium', 'high'), true) ? $value : 'auto';
2613 - }
2614 -
2615 - // Integer field: images per article (clamp 1–5)
2616 - if ($field === 'content_image_count') {
2617 - $value = (string) max(1, min(5, (int) $value));
2618 - }
2619 -
2620 2401 $options = get_option('mxchat_options', array());
2621 2402 $options[$field] = $value;
2622 2403 update_option('mxchat_options', $options);
2623 2404
@@ -3359,39 +3140,23 @@
3359 3140 array('key' => '_mxchat_seo_score', 'compare' => 'NOT EXISTS'),
3360 3141 );
3361 3142 }
3362 3143
3363 - // When sorting by a meta field, posts WITHOUT the key must stay in the
3364 - // list — GSC meta is no longer zero-filled for no-data posts (plan
3365 - // 282c80), and score meta never existed for unanalyzed posts. A single
3366 - // OR EXISTS/NOT-EXISTS meta_query cannot order this reliably (under an
3367 - // OR relation WP_Meta_Query leaves the orderby clause's join alias
3368 - // unconstrained by meta_key, so ORDER BY reads arbitrary meta rows).
3369 - // Instead: two id-only queries — posts WITH the key sorted by its
3370 - // value, then posts WITHOUT it appended (newest first). Missing-data
3371 - // rows deliberately sort last in BOTH directions.
3144 + // When sorting by a meta field, ensure meta_key is set for ordering.
3145 + // For 'all' filter, include posts without the meta key via OR clause.
3372 3146 if ($sort_meta_key) {
3373 - $with_args = $args;
3374 - $with_args['meta_key'] = $sort_meta_key;
3375 - $with_args['orderby'] = 'meta_value_num';
3376 - $q_with = new \WP_Query($with_args);
3377 -
3378 - $without_args = $args;
3379 - $missing_clause = array('key' => $sort_meta_key, 'compare' => 'NOT EXISTS');
3380 - if (!empty($without_args['meta_query'])) {
3381 - $without_args['meta_query'] = array('relation' => 'AND', $without_args['meta_query'], $missing_clause);
3382 - } else {
3383 - $without_args['meta_query'] = array($missing_clause);
3147 + $args['meta_key'] = $sort_meta_key;
3148 + if ($filter === 'all') {
3149 + $args['meta_query'] = array(
3150 + 'relation' => 'OR',
3151 + array('key' => $sort_meta_key, 'compare' => 'EXISTS'),
3152 + array('key' => $sort_meta_key, 'compare' => 'NOT EXISTS'),
3153 + );
3384 3154 }
3385 - $without_args['orderby'] = 'date';
3386 - $without_args['order'] = 'DESC';
3387 - $q_without = new \WP_Query($without_args);
3155 + }
3388 3156
3389 - $all_ids = array_merge($q_with->posts, $q_without->posts);
3390 - } else {
3391 - $query = new \WP_Query($args);
3392 - $all_ids = $query->posts;
3393 - }
3157 + $query = new \WP_Query($args);
3158 + $all_ids = $query->posts;
3394 3159 $total = count($all_ids);
3395 3160 $pages = max(1, ceil($total / $per_page));
3396 3161 $page = min($page, $pages);
3397 3162 $offset = ($page - 1) * $per_page;