Backend improvements: - Add cache auto-clear on settings save (class-settings-v2.php) - Hooks updated_option action - Clears connection test transients when local backend settings change - Add reasoning_content streaming support (class-local-backend-provider.php) - Handles thinking models like Claude extended thinking - Captures chunk['choices'][0]['delta']['reasoning_content'] Documentation: - Add FRONTEND_AND_CHAT_FIX_SUMMARY.md with all fixes - Add FRONTEND-REFACTOR-PHASE2.md with modularization plan Note: Splitting effort deferred - will continue iterating on monolith Next: Fix session history not appearing bug
2103 lines
72 KiB
PHP
2103 lines
72 KiB
PHP
<?php
|
|
/**
|
|
* Settings Page V2
|
|
*
|
|
* Refactored settings page with Agentic design tokens and separated view files.
|
|
*
|
|
* @package WP_Agentic_Writer
|
|
*/
|
|
|
|
if (!defined("ABSPATH")) {
|
|
exit();
|
|
}
|
|
|
|
/**
|
|
* Class WP_Agentic_Writer_Settings_V2
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
class WP_Agentic_Writer_Settings_V2
|
|
{
|
|
/**
|
|
* Get singleton instance.
|
|
*
|
|
* @since 0.2.0
|
|
* @return WP_Agentic_Writer_Settings_V2
|
|
*/
|
|
public static function get_instance()
|
|
{
|
|
static $instance = null;
|
|
|
|
if (null === $instance) {
|
|
$instance = new self();
|
|
}
|
|
|
|
return $instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
private function __construct()
|
|
{
|
|
add_action("admin_menu", [$this, "add_settings_page"]);
|
|
add_action("admin_init", [$this, "register_settings"]);
|
|
add_action("admin_enqueue_scripts", [$this, "enqueue_scripts"]);
|
|
add_action("wp_ajax_wpaw_refresh_models", [
|
|
$this,
|
|
"ajax_refresh_models",
|
|
]);
|
|
add_action("wp_ajax_wpaw_get_cost_log_data", [
|
|
$this,
|
|
"ajax_get_cost_log_data",
|
|
]);
|
|
add_action("wp_ajax_wpaw_get_header_stats", [
|
|
$this,
|
|
"ajax_get_header_stats",
|
|
]);
|
|
add_action("wp_ajax_wpaw_test_api_connection", [
|
|
$this,
|
|
"ajax_test_api_connection",
|
|
]);
|
|
add_action("wp_ajax_wpaw_debug_models", [$this, "ajax_debug_models"]);
|
|
add_action("wp_ajax_wpaw_save_custom_model", [
|
|
$this,
|
|
"ajax_save_custom_model",
|
|
]);
|
|
add_action("wp_ajax_wpaw_delete_custom_model", [
|
|
$this,
|
|
"ajax_delete_custom_model",
|
|
]);
|
|
add_action("wp_ajax_wpaw_test_local_backend", [
|
|
$this,
|
|
"ajax_test_local_backend",
|
|
]);
|
|
add_action("wp_ajax_wpaw_test_memanto", [$this, "ajax_test_memanto"]);
|
|
|
|
// Clear connection test cache when local backend settings change
|
|
add_action(
|
|
"updated_option",
|
|
[$this, "clear_local_backend_cache_on_settings_change"],
|
|
10,
|
|
3,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Clear the local backend connection test cache when relevant settings change.
|
|
*
|
|
* This ensures that after a user updates their local backend URL, API key,
|
|
* or model settings, the cached "connection test" result is invalidated
|
|
* so the next chat request will re-test the connection.
|
|
*
|
|
* @since 0.2.0
|
|
* @param string $option Option name that was updated.
|
|
* @param mixed $old_value Old option value.
|
|
* @param mixed $new_value New option value.
|
|
*/
|
|
public function clear_local_backend_cache_on_settings_change(
|
|
$option,
|
|
$old_value,
|
|
$new_value,
|
|
) {
|
|
// Only handle our settings option
|
|
if ("wp_agentic_writer_settings" !== $option) {
|
|
return;
|
|
}
|
|
|
|
// Check if any local backend-related setting changed
|
|
$local_backend_keys = [
|
|
"local_backend_url",
|
|
"local_backend_key",
|
|
"local_backend_image_url",
|
|
"local_backend_image_key",
|
|
"local_backend_model",
|
|
"local_backend_models",
|
|
"local_backend_enabled",
|
|
"local_backend_image_enabled",
|
|
];
|
|
|
|
$old_value = is_array($old_value) ? $old_value : [];
|
|
$new_value = is_array($new_value) ? $new_value : [];
|
|
|
|
foreach ($local_backend_keys as $key) {
|
|
$old = $old_value[$key] ?? null;
|
|
$new = $new_value[$key] ?? null;
|
|
|
|
// Check if the value changed (handle both array and scalar comparisons)
|
|
if ($old !== $new) {
|
|
// Arrays need special comparison
|
|
if (is_array($old) && is_array($new)) {
|
|
if (serialize($old) !== serialize($new)) {
|
|
$this->do_clear_local_backend_cache();
|
|
return;
|
|
}
|
|
} else {
|
|
$this->do_clear_local_backend_cache();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actually clear the cache and log the action.
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
private function do_clear_local_backend_cache()
|
|
{
|
|
if (!class_exists("WP_Agentic_Writer_Provider_Manager")) {
|
|
return;
|
|
}
|
|
|
|
$count = WP_Agentic_Writer_Provider_Manager::clear_connection_test_cache();
|
|
|
|
if (defined("WP_DEBUG") && WP_DEBUG) {
|
|
error_log(
|
|
"WPAW Settings: Local backend settings changed. Cleared " .
|
|
$count .
|
|
" connection test cache entries.",
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enqueue scripts for settings page.
|
|
*
|
|
* @since 0.2.0
|
|
* @param string $hook Current admin page hook.
|
|
*/
|
|
public function enqueue_scripts($hook)
|
|
{
|
|
if ("settings_page_wp-agentic-writer-settings" !== $hook) {
|
|
return;
|
|
}
|
|
|
|
$settings_css_path =
|
|
WP_AGENTIC_WRITER_DIR . "views/settings-v2/style.css";
|
|
wp_enqueue_style(
|
|
"wpaw-settings-v2-stitch",
|
|
WP_AGENTIC_WRITER_URL . "views/settings-v2/style.css",
|
|
[],
|
|
file_exists($settings_css_path)
|
|
? filemtime($settings_css_path)
|
|
: WP_AGENTIC_WRITER_VERSION,
|
|
);
|
|
|
|
wp_enqueue_style(
|
|
"wpaw-select2",
|
|
WP_AGENTIC_WRITER_URL . "assets/vendor/select2/select2.min.css",
|
|
[],
|
|
"4.1.0-rc.0",
|
|
);
|
|
|
|
wp_enqueue_script(
|
|
"wpaw-select2",
|
|
WP_AGENTIC_WRITER_URL . "assets/vendor/select2/select2.min.js",
|
|
["jquery"],
|
|
"4.1.0-rc.0",
|
|
true,
|
|
);
|
|
|
|
wp_enqueue_script(
|
|
"wp-agentic-writer-settings-v2",
|
|
WP_AGENTIC_WRITER_URL . "assets/js/settings-v2-stitch.js",
|
|
["jquery", "wpaw-select2"],
|
|
WP_AGENTIC_WRITER_VERSION,
|
|
true,
|
|
);
|
|
|
|
$settings = get_option("wp_agentic_writer_settings", []);
|
|
wp_localize_script("wp-agentic-writer-settings-v2", "wpawSettingsV2", [
|
|
"ajaxUrl" => admin_url("admin-ajax.php"),
|
|
"nonce" => wp_create_nonce("wpaw_settings"),
|
|
"models" => $this->get_models_for_select(),
|
|
"currentModels" => [
|
|
"planning" =>
|
|
$settings["planning_model"] ??
|
|
WPAW_Model_Registry::get_default_model("planning"),
|
|
"writing" =>
|
|
$settings["writing_model"] ??
|
|
($settings["execution_model"] ??
|
|
WPAW_Model_Registry::get_default_model("writing")),
|
|
"execution" =>
|
|
$settings["writing_model"] ??
|
|
($settings["execution_model"] ??
|
|
WPAW_Model_Registry::get_default_model("writing")),
|
|
"clarity" =>
|
|
$settings["clarity_model"] ??
|
|
WPAW_Model_Registry::get_default_model("clarity"),
|
|
"refinement" =>
|
|
$settings["refinement_model"] ??
|
|
WPAW_Model_Registry::get_default_model("refinement"),
|
|
"chat" =>
|
|
$settings["chat_model"] ??
|
|
WPAW_Model_Registry::get_default_model("chat"),
|
|
"image" =>
|
|
$settings["image_model"] ??
|
|
WPAW_Model_Registry::get_default_model("image"),
|
|
],
|
|
"presets" => $this->get_model_presets(),
|
|
"i18n" => [
|
|
"refreshing" => __("Refreshing...", "wp-agentic-writer"),
|
|
"refreshModels" => __("Refresh Models", "wp-agentic-writer"),
|
|
"saveSuccess" => __(
|
|
"Settings saved successfully!",
|
|
"wp-agentic-writer",
|
|
),
|
|
"saveError" => __(
|
|
"Error saving settings.",
|
|
"wp-agentic-writer",
|
|
),
|
|
"confirmReset" => __(
|
|
"Are you sure you want to reset all settings to defaults?",
|
|
"wp-agentic-writer",
|
|
),
|
|
"loading" => __("Loading...", "wp-agentic-writer"),
|
|
"noResults" => __("No models found", "wp-agentic-writer"),
|
|
"searchPlaceholder" => __(
|
|
"Search models...",
|
|
"wp-agentic-writer",
|
|
),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get curated model presets (centralized source).
|
|
*
|
|
* These are intentional product decisions for different budget tiers.
|
|
* Model IDs may differ from registry defaults to balance cost/quality.
|
|
*
|
|
* @since 0.2.0
|
|
* @return array Curated model presets.
|
|
*/
|
|
public function get_model_presets()
|
|
{
|
|
return [
|
|
"budget" => [
|
|
"chat" => "google/gemini-2.5-flash",
|
|
"clarity" => "google/gemini-2.5-flash",
|
|
"planning" => "google/gemini-2.5-flash",
|
|
"writing" => "mistralai/mistral-small-creative",
|
|
"refinement" => "google/gemini-2.5-flash",
|
|
"image" => "openai/gpt-4o",
|
|
],
|
|
"balanced" => [
|
|
"chat" => "google/gemini-2.5-flash",
|
|
"clarity" => "google/gemini-2.5-flash",
|
|
"planning" => "google/gemini-2.5-flash",
|
|
"writing" => "anthropic/claude-sonnet-4",
|
|
"refinement" => "anthropic/claude-sonnet-4",
|
|
"image" => "openai/gpt-4o",
|
|
],
|
|
"premium" => [
|
|
"chat" => "google/gemini-3-flash-preview",
|
|
"clarity" => "anthropic/claude-sonnet-4",
|
|
"planning" => "google/gemini-3-flash-preview",
|
|
"writing" => "openai/gpt-4.1",
|
|
"refinement" => "openai/gpt-4.1",
|
|
"image" => "openai/gpt-4o",
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get models for select dropdowns.
|
|
*
|
|
* @since 0.2.0
|
|
* @return array Models grouped by category.
|
|
*/
|
|
public function get_models_for_select()
|
|
{
|
|
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
|
|
$models = $provider->get_cached_models();
|
|
|
|
if (is_wp_error($models)) {
|
|
return $this->get_fallback_models();
|
|
}
|
|
|
|
$transformed = $this->transform_models_for_js($models);
|
|
|
|
// Debug logging
|
|
if (defined("WP_DEBUG") && WP_DEBUG) {
|
|
$custom_models = get_option("wp_agentic_writer_custom_models", []);
|
|
error_log(
|
|
"WPAW get_models_for_select: custom_models in DB = " .
|
|
wp_json_encode($custom_models),
|
|
);
|
|
error_log(
|
|
"WPAW get_models_for_select: image models count = " .
|
|
count($transformed["image"]["all"] ?? []),
|
|
);
|
|
}
|
|
|
|
return $transformed;
|
|
}
|
|
|
|
/**
|
|
* Format model name from ID.
|
|
*
|
|
* @since 0.2.0
|
|
* @param string $model_id Model ID.
|
|
* @return string Formatted model name.
|
|
*/
|
|
private function format_model_name($model_id)
|
|
{
|
|
// Remove provider prefix
|
|
$parts = explode("/", $model_id);
|
|
$name = end($parts);
|
|
|
|
// Remove :free suffix
|
|
$name = preg_replace('/:free$/i', "", $name);
|
|
|
|
// Convert hyphens and underscores to spaces
|
|
$name = str_replace(["-", "_"], " ", $name);
|
|
|
|
// Capitalize words
|
|
$name = ucwords($name);
|
|
|
|
// Add provider prefix back
|
|
if (count($parts) > 1) {
|
|
$provider = ucfirst($parts[0]);
|
|
$name = $provider . ": " . $name;
|
|
}
|
|
|
|
return $name;
|
|
}
|
|
|
|
/**
|
|
* Get fallback models when API fails.
|
|
*
|
|
* @since 0.2.0
|
|
* @return array Fallback model structure.
|
|
*/
|
|
private function get_fallback_models()
|
|
{
|
|
return [
|
|
"planning" => [
|
|
"recommended" => [
|
|
[
|
|
"id" => WPAW_Model_Registry::get_default_model(
|
|
"planning",
|
|
),
|
|
"name" => WPAW_Model_Registry::get_model_display_name(
|
|
WPAW_Model_Registry::get_default_model("planning"),
|
|
),
|
|
],
|
|
],
|
|
"all" => [
|
|
[
|
|
"id" => WPAW_Model_Registry::get_default_model(
|
|
"planning",
|
|
),
|
|
"name" => WPAW_Model_Registry::get_model_display_name(
|
|
WPAW_Model_Registry::get_default_model("planning"),
|
|
),
|
|
],
|
|
],
|
|
],
|
|
"execution" => [
|
|
"recommended" => [
|
|
[
|
|
"id" => WPAW_Model_Registry::get_fallback_model(
|
|
"execution",
|
|
),
|
|
"name" => WPAW_Model_Registry::get_model_display_name(
|
|
WPAW_Model_Registry::get_fallback_model(
|
|
"execution",
|
|
),
|
|
),
|
|
],
|
|
],
|
|
"all" => [
|
|
[
|
|
"id" => WPAW_Model_Registry::get_fallback_model(
|
|
"execution",
|
|
),
|
|
"name" => WPAW_Model_Registry::get_model_display_name(
|
|
WPAW_Model_Registry::get_fallback_model(
|
|
"execution",
|
|
),
|
|
),
|
|
],
|
|
],
|
|
],
|
|
"image" => [
|
|
"recommended" => [
|
|
[
|
|
"id" => WPAW_Model_Registry::get_default_model("image"),
|
|
"name" => WPAW_Model_Registry::get_model_display_name(
|
|
WPAW_Model_Registry::get_default_model("image"),
|
|
),
|
|
],
|
|
],
|
|
"all" => [
|
|
[
|
|
"id" => WPAW_Model_Registry::get_default_model("image"),
|
|
"name" => WPAW_Model_Registry::get_model_display_name(
|
|
WPAW_Model_Registry::get_default_model("image"),
|
|
),
|
|
],
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Transform models structure for JavaScript consumption.
|
|
*
|
|
* @since 0.2.0
|
|
* @param array $models Models from provider.
|
|
* @return array Transformed models.
|
|
*/
|
|
private function transform_models_for_js($models)
|
|
{
|
|
// Handle flat model list from OpenRouter
|
|
if (
|
|
!empty($models) &&
|
|
array_keys($models) === range(0, count($models) - 1)
|
|
) {
|
|
$settings = get_option("wp_agentic_writer_settings", []);
|
|
$planning_id =
|
|
$settings["planning_model"] ??
|
|
WPAW_Model_Registry::get_default_model("planning");
|
|
$execution_id =
|
|
$settings["execution_model"] ??
|
|
WPAW_Model_Registry::get_default_model("execution");
|
|
$image_id =
|
|
$settings["image_model"] ??
|
|
WPAW_Model_Registry::get_default_model("image");
|
|
|
|
$text_models = [];
|
|
$image_models = [];
|
|
|
|
// Categorize models using OpenRouter's output_modalities field
|
|
foreach ($models as $model) {
|
|
if (empty($model["id"])) {
|
|
continue;
|
|
}
|
|
|
|
$prompt_price = isset($model["pricing"]["prompt"])
|
|
? (float) $model["pricing"]["prompt"]
|
|
: 0;
|
|
$completion_price = isset($model["pricing"]["completion"])
|
|
? (float) $model["pricing"]["completion"]
|
|
: 0;
|
|
$image_price = isset($model["pricing"]["image"])
|
|
? (float) $model["pricing"]["image"]
|
|
: 0;
|
|
|
|
$model_data = [
|
|
"id" => $model["id"],
|
|
"name" => $model["name"] ?? $model["id"],
|
|
"is_free" =>
|
|
$prompt_price <= 0.0 &&
|
|
$completion_price <= 0.0 &&
|
|
$image_price <= 0.0,
|
|
"pricing" => [
|
|
"prompt" => $prompt_price,
|
|
"completion" => $completion_price,
|
|
"image" => $image_price,
|
|
],
|
|
];
|
|
|
|
// Use OpenRouter's output_modalities to categorize - trust OpenRouter's classification
|
|
$output_modalities =
|
|
$model["architecture"]["output_modalities"] ?? [];
|
|
|
|
// Image generation models have 'image' in output_modalities
|
|
if (in_array("image", $output_modalities, true)) {
|
|
$image_models[] = $model_data;
|
|
}
|
|
|
|
// Text models have 'text' in output_modalities (most models)
|
|
if (in_array("text", $output_modalities, true)) {
|
|
$text_models[] = $model_data;
|
|
}
|
|
}
|
|
|
|
$chat_id =
|
|
$settings["chat_model"] ??
|
|
WPAW_Model_Registry::get_default_model("chat");
|
|
$clarity_id =
|
|
$settings["clarity_model"] ??
|
|
WPAW_Model_Registry::get_default_model("clarity");
|
|
$refinement_id =
|
|
$settings["refinement_model"] ??
|
|
WPAW_Model_Registry::get_default_model("refinement");
|
|
$writing_id =
|
|
$settings["writing_model"] ??
|
|
($settings["execution_model"] ??
|
|
WPAW_Model_Registry::get_default_model("writing"));
|
|
|
|
// Add currently selected models to text_models if not already present
|
|
$current_model_ids = [
|
|
$planning_id,
|
|
$execution_id,
|
|
$chat_id,
|
|
$clarity_id,
|
|
$refinement_id,
|
|
$writing_id,
|
|
];
|
|
foreach ($current_model_ids as $model_id) {
|
|
$found = false;
|
|
foreach ($text_models as $tm) {
|
|
if ($tm["id"] === $model_id) {
|
|
$found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$found && !empty($model_id)) {
|
|
$text_models[] = [
|
|
"id" => $model_id,
|
|
"name" => $this->format_model_name($model_id),
|
|
"is_free" => false,
|
|
"pricing" => [
|
|
"prompt" => 0,
|
|
"completion" => 0,
|
|
"image" => 0,
|
|
],
|
|
];
|
|
}
|
|
}
|
|
|
|
// Add currently selected image model to image_models if not already present
|
|
if (!empty($image_id)) {
|
|
$found = false;
|
|
foreach ($image_models as $im) {
|
|
if ($im["id"] === $image_id) {
|
|
$found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$found) {
|
|
$image_models[] = [
|
|
"id" => $image_id,
|
|
"name" => $this->format_model_name($image_id),
|
|
"is_free" => false,
|
|
"pricing" => [
|
|
"prompt" => 0,
|
|
"completion" => 0,
|
|
"image" => 0,
|
|
],
|
|
];
|
|
}
|
|
}
|
|
|
|
// Add user's custom models (not listed in API but callable by ID)
|
|
$custom_models = get_option("wp_agentic_writer_custom_models", []);
|
|
foreach ($custom_models as $custom) {
|
|
if (empty($custom["id"])) {
|
|
continue;
|
|
}
|
|
$custom_model_data = [
|
|
"id" => $custom["id"],
|
|
"name" => !empty($custom["name"])
|
|
? $custom["name"]
|
|
: $this->format_model_name($custom["id"]),
|
|
"is_free" => false,
|
|
"is_custom" => true,
|
|
"pricing" => [
|
|
"prompt" => 0,
|
|
"completion" => 0,
|
|
"image" => 0,
|
|
],
|
|
];
|
|
|
|
$type = $custom["type"] ?? "text";
|
|
if ("image" === $type) {
|
|
$image_models[] = $custom_model_data;
|
|
} else {
|
|
$text_models[] = $custom_model_data;
|
|
}
|
|
}
|
|
|
|
// Now create find_model closure after all models are added
|
|
$find_model = function ($model_id) use (
|
|
$text_models,
|
|
$image_models,
|
|
) {
|
|
foreach (array_merge($text_models, $image_models) as $model) {
|
|
if ($model["id"] === $model_id) {
|
|
return $model;
|
|
}
|
|
}
|
|
// If model not found, create a fallback entry
|
|
if (!empty($model_id)) {
|
|
return [
|
|
"id" => $model_id,
|
|
"name" => $this->format_model_name($model_id),
|
|
"is_free" => false,
|
|
"pricing" => [
|
|
"prompt" => 0,
|
|
"completion" => 0,
|
|
"image" => 0,
|
|
],
|
|
];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
return [
|
|
"planning" => [
|
|
"recommended" => array_filter([$find_model($planning_id)]),
|
|
"all" => $text_models,
|
|
],
|
|
"execution" => [
|
|
"recommended" => array_filter([$find_model($execution_id)]),
|
|
"all" => $text_models,
|
|
],
|
|
"chat" => [
|
|
"recommended" => array_filter([$find_model($chat_id)]),
|
|
"all" => $text_models,
|
|
],
|
|
"image" => [
|
|
"recommended" => array_filter([$find_model($image_id)]),
|
|
"all" => $image_models,
|
|
],
|
|
];
|
|
}
|
|
|
|
$transformed = [];
|
|
|
|
foreach ($models as $type => $categories) {
|
|
if (!isset($transformed[$type])) {
|
|
$transformed[$type] = [
|
|
"recommended" => [],
|
|
"all" => [],
|
|
];
|
|
}
|
|
|
|
// Combine free and paid into 'all' array
|
|
$all_models = array_merge(
|
|
$categories["free"] ?? [],
|
|
$categories["paid"] ?? [],
|
|
);
|
|
|
|
// Remove duplicates
|
|
$recommended_ids = [];
|
|
foreach ($categories["recommended"] ?? [] as $model) {
|
|
$transformed[$type]["recommended"][] = $model;
|
|
$recommended_ids[$model["id"]] = true;
|
|
}
|
|
|
|
// Add all models, avoiding duplicates with recommended
|
|
foreach ($all_models as $model) {
|
|
if (!isset($recommended_ids[$model["id"]])) {
|
|
$transformed[$type]["all"][] = $model;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $transformed;
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for refreshing models.
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
public function ajax_refresh_models()
|
|
{
|
|
if (!check_ajax_referer("wpaw_settings", "nonce", false)) {
|
|
wp_send_json_error(["message" => "Invalid nonce"]);
|
|
return;
|
|
}
|
|
|
|
if (!current_user_can("manage_options")) {
|
|
wp_send_json_error(["message" => "Permission denied"]);
|
|
}
|
|
|
|
$settings = get_option("wp_agentic_writer_settings", []);
|
|
$posted_api_key = isset($_POST["api_key"])
|
|
? trim(sanitize_text_field(wp_unslash($_POST["api_key"])))
|
|
: "";
|
|
$api_key = !empty($posted_api_key)
|
|
? $posted_api_key
|
|
: $settings["openrouter_api_key"] ?? "";
|
|
|
|
$provider = !empty($posted_api_key)
|
|
? WP_Agentic_Writer_OpenRouter_Provider::for_api_key($api_key)
|
|
: WP_Agentic_Writer_OpenRouter_Provider::get_instance();
|
|
$models = $provider->fetch_and_cache_models(true);
|
|
|
|
if (is_wp_error($models)) {
|
|
wp_send_json_error(["message" => $models->get_error_message()]);
|
|
}
|
|
|
|
$transformed = $this->transform_models_for_js($models);
|
|
|
|
wp_send_json_success([
|
|
"models" => $transformed,
|
|
"message" => __(
|
|
"Models refreshed successfully!",
|
|
"wp-agentic-writer",
|
|
),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for saving a custom model.
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
public function ajax_save_custom_model()
|
|
{
|
|
if (!check_ajax_referer("wpaw_settings", "nonce", false)) {
|
|
wp_send_json_error(["message" => "Invalid nonce"]);
|
|
return;
|
|
}
|
|
|
|
if (!current_user_can("manage_options")) {
|
|
wp_send_json_error(["message" => "Permission denied"]);
|
|
return;
|
|
}
|
|
|
|
$model_id = isset($_POST["model_id"])
|
|
? sanitize_text_field($_POST["model_id"])
|
|
: "";
|
|
$model_name = isset($_POST["model_name"])
|
|
? sanitize_text_field($_POST["model_name"])
|
|
: "";
|
|
$model_type = isset($_POST["model_type"])
|
|
? sanitize_text_field($_POST["model_type"])
|
|
: "text";
|
|
|
|
if (empty($model_id)) {
|
|
wp_send_json_error(["message" => "Model ID is required"]);
|
|
return;
|
|
}
|
|
|
|
// Use separate option for custom models
|
|
$custom_models = get_option("wp_agentic_writer_custom_models", []);
|
|
|
|
// Check if model already exists, update it
|
|
$found = false;
|
|
foreach ($custom_models as $index => $cm) {
|
|
if ($cm["id"] === $model_id) {
|
|
$custom_models[$index] = [
|
|
"id" => $model_id,
|
|
"name" => $model_name,
|
|
"type" => $model_type,
|
|
];
|
|
$found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Add new model if not found
|
|
if (!$found) {
|
|
$custom_models[] = [
|
|
"id" => $model_id,
|
|
"name" => $model_name,
|
|
"type" => $model_type,
|
|
];
|
|
}
|
|
|
|
$saved = update_option(
|
|
"wp_agentic_writer_custom_models",
|
|
array_values($custom_models),
|
|
);
|
|
|
|
// Get fresh combined models for Select2
|
|
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
|
|
$models = $provider->get_cached_models();
|
|
if (is_wp_error($models)) {
|
|
$models = [];
|
|
}
|
|
$transformed = $this->transform_models_for_js($models);
|
|
|
|
wp_send_json_success([
|
|
"message" => __("Custom model saved!", "wp-agentic-writer"),
|
|
"models" => $transformed,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for deleting a custom model.
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
public function ajax_delete_custom_model()
|
|
{
|
|
if (!check_ajax_referer("wpaw_settings", "nonce", false)) {
|
|
wp_send_json_error(["message" => "Invalid nonce"]);
|
|
return;
|
|
}
|
|
|
|
if (!current_user_can("manage_options")) {
|
|
wp_send_json_error(["message" => "Permission denied"]);
|
|
return;
|
|
}
|
|
|
|
$model_id = isset($_POST["model_id"])
|
|
? sanitize_text_field($_POST["model_id"])
|
|
: "";
|
|
|
|
if (empty($model_id)) {
|
|
wp_send_json_error(["message" => "Model ID is required"]);
|
|
return;
|
|
}
|
|
|
|
// Use separate option for custom models
|
|
$custom_models = get_option("wp_agentic_writer_custom_models", []);
|
|
|
|
// Remove the model
|
|
$custom_models = array_filter($custom_models, function ($cm) use (
|
|
$model_id,
|
|
) {
|
|
return $cm["id"] !== $model_id;
|
|
});
|
|
|
|
update_option(
|
|
"wp_agentic_writer_custom_models",
|
|
array_values($custom_models),
|
|
);
|
|
|
|
// Get fresh combined models for Select2
|
|
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
|
|
$models = $provider->get_cached_models();
|
|
if (is_wp_error($models)) {
|
|
$models = [];
|
|
}
|
|
$transformed = $this->transform_models_for_js($models);
|
|
|
|
wp_send_json_success([
|
|
"message" => __("Custom model deleted!", "wp-agentic-writer"),
|
|
"models" => $transformed,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for getting cost log data (server-side pagination).
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
public function ajax_get_cost_log_data()
|
|
{
|
|
if (!check_ajax_referer("wpaw_settings", "nonce", false)) {
|
|
wp_send_json_error(["message" => "Invalid nonce"]);
|
|
return;
|
|
}
|
|
|
|
if (!current_user_can("manage_options")) {
|
|
wp_send_json_error(["message" => "Permission denied"]);
|
|
return;
|
|
}
|
|
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . "wpaw_cost_tracking";
|
|
|
|
// Check if table exists
|
|
$table_exists =
|
|
$wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") === $table_name;
|
|
|
|
if (!$table_exists) {
|
|
wp_send_json_success([
|
|
"records" => [],
|
|
"total_items" => 0,
|
|
"total_pages" => 0,
|
|
"current_page" => 1,
|
|
"per_page" => 25,
|
|
"stats" => [
|
|
"all_time" => "0.0000",
|
|
"monthly" => "0.0000",
|
|
"today" => "0.0000",
|
|
"avg_per_post" => "0.0000",
|
|
"action_summary" => [],
|
|
],
|
|
"filters" => [
|
|
"models" => [],
|
|
"types" => [],
|
|
],
|
|
]);
|
|
return;
|
|
}
|
|
|
|
// Get parameters
|
|
$page = isset($_POST["page"]) ? max(1, intval($_POST["page"])) : 1;
|
|
$per_page = isset($_POST["per_page"])
|
|
? min(100, max(10, intval($_POST["per_page"])))
|
|
: 25;
|
|
$offset = ($page - 1) * $per_page;
|
|
|
|
// Filters
|
|
$filter_post = isset($_POST["filter_post"])
|
|
? intval($_POST["filter_post"])
|
|
: 0;
|
|
$filter_model = isset($_POST["filter_model"])
|
|
? sanitize_text_field($_POST["filter_model"])
|
|
: "";
|
|
$filter_type = isset($_POST["filter_type"])
|
|
? sanitize_text_field($_POST["filter_type"])
|
|
: "";
|
|
$filter_date_from = isset($_POST["filter_date_from"])
|
|
? sanitize_text_field($_POST["filter_date_from"])
|
|
: "";
|
|
$filter_date_to = isset($_POST["filter_date_to"])
|
|
? sanitize_text_field($_POST["filter_date_to"])
|
|
: "";
|
|
|
|
// Build WHERE clause (OpenRouter-only for this OpenRouter cost log screen).
|
|
$where = ["provider = 'openrouter'"];
|
|
if ($filter_post > 0) {
|
|
$where[] = $wpdb->prepare("post_id = %d", $filter_post);
|
|
}
|
|
if (!empty($filter_model)) {
|
|
$where[] = $wpdb->prepare("model = %s", $filter_model);
|
|
}
|
|
if (!empty($filter_type)) {
|
|
$where[] = $wpdb->prepare("action = %s", $filter_type);
|
|
}
|
|
if (!empty($filter_date_from)) {
|
|
$where[] = $wpdb->prepare(
|
|
"DATE(created_at) >= %s",
|
|
$filter_date_from,
|
|
);
|
|
}
|
|
if (!empty($filter_date_to)) {
|
|
$where[] = $wpdb->prepare(
|
|
"DATE(created_at) <= %s",
|
|
$filter_date_to,
|
|
);
|
|
}
|
|
$where_clause = implode(" AND ", $where);
|
|
|
|
// Get total count of distinct posts
|
|
$total_items = $wpdb->get_var(
|
|
"SELECT COUNT(DISTINCT post_id) FROM {$table_name} WHERE {$where_clause}",
|
|
);
|
|
$total_pages = ceil($total_items / $per_page);
|
|
|
|
// Optimized: Get grouped records with aggregation in SQL.
|
|
// This pushes grouping and ordering to the database instead of PHP.
|
|
$grouped_records_sql = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT
|
|
post_id,
|
|
SUM(cost) as total_cost,
|
|
COUNT(*) as call_count,
|
|
MAX(created_at) as last_call
|
|
FROM {$table_name}
|
|
WHERE {$where_clause}
|
|
GROUP BY post_id
|
|
ORDER BY post_id DESC
|
|
LIMIT %d OFFSET %d",
|
|
$per_page,
|
|
$offset,
|
|
),
|
|
ARRAY_A,
|
|
);
|
|
|
|
// Build grouped records with post details
|
|
$formatted_records = [];
|
|
$post_ids = [];
|
|
|
|
foreach ($grouped_records_sql as $row) {
|
|
$post_id = (int) $row["post_id"];
|
|
$post_ids[] = $post_id;
|
|
|
|
if ($post_id > 0) {
|
|
$post_title = get_the_title($post_id);
|
|
if (!$post_title) {
|
|
$post_title = sprintf(
|
|
__("[Removed Post #%d]", "wp-agentic-writer"),
|
|
$post_id,
|
|
);
|
|
$post_link = "";
|
|
} else {
|
|
$post_link = get_edit_post_link($post_id, "raw");
|
|
}
|
|
} else {
|
|
$post_title = __("System/Other", "wp-agentic-writer");
|
|
$post_link = "";
|
|
}
|
|
|
|
$formatted_records[] = [
|
|
"post_id" => $post_id,
|
|
"post_title" => $post_title,
|
|
"post_link" => $post_link,
|
|
"total_cost" => number_format((float) $row["total_cost"], 4),
|
|
"call_count" => (int) $row["call_count"],
|
|
"last_call" => date_i18n(
|
|
"Y-m-d H:i:s",
|
|
strtotime($row["last_call"]),
|
|
),
|
|
"details" => [], // Lazy-loaded on expand
|
|
];
|
|
}
|
|
|
|
// Load detail rows for visible posts.
|
|
// This keeps expand/collapse usable without requiring a second endpoint.
|
|
if (!empty($post_ids)) {
|
|
$placeholders = implode(",", array_fill(0, count($post_ids), "%d"));
|
|
$details_sql = $wpdb->prepare(
|
|
"SELECT post_id, created_at, model, action, input_tokens, output_tokens, cost
|
|
FROM {$table_name}
|
|
WHERE provider = 'openrouter' AND post_id IN ({$placeholders})
|
|
ORDER BY created_at DESC",
|
|
...$post_ids,
|
|
);
|
|
$detail_rows = $wpdb->get_results($details_sql, ARRAY_A);
|
|
|
|
$image_variants_table = $wpdb->prefix . "wpaw_images_variants";
|
|
$image_variants_table_exists =
|
|
$wpdb->get_var("SHOW TABLES LIKE '{$image_variants_table}'") ===
|
|
$image_variants_table;
|
|
if ($image_variants_table_exists) {
|
|
// Find posts that already have image_generation in wpaw_cost_tracking
|
|
// to avoid duplicates from the variants table.
|
|
$posts_with_tracked_images = [];
|
|
foreach ($detail_rows as $existing) {
|
|
if (($existing["action"] ?? "") === "image_generation") {
|
|
$posts_with_tracked_images[
|
|
(int) ($existing["post_id"] ?? 0)
|
|
] = true;
|
|
}
|
|
}
|
|
$variant_post_ids = array_filter($post_ids, function (
|
|
$pid,
|
|
) use ($posts_with_tracked_images) {
|
|
return empty($posts_with_tracked_images[(int) $pid]);
|
|
});
|
|
if (!empty($variant_post_ids)) {
|
|
$variant_placeholders = implode(
|
|
",",
|
|
array_fill(0, count($variant_post_ids), "%d"),
|
|
);
|
|
$image_details_sql = $wpdb->prepare(
|
|
"SELECT post_id, created_at, image_model_used AS model, cost
|
|
FROM {$image_variants_table}
|
|
WHERE post_id IN ({$variant_placeholders}) AND cost IS NOT NULL AND cost > 0
|
|
ORDER BY created_at DESC",
|
|
...$variant_post_ids,
|
|
);
|
|
$image_detail_rows = $wpdb->get_results(
|
|
$image_details_sql,
|
|
ARRAY_A,
|
|
);
|
|
foreach ($image_detail_rows as $image_detail_row) {
|
|
$detail_rows[] = [
|
|
"post_id" =>
|
|
(int) ($image_detail_row["post_id"] ?? 0),
|
|
"created_at" =>
|
|
$image_detail_row["created_at"] ?? "",
|
|
"model" => $image_detail_row["model"] ?? "",
|
|
"action" => "image_generation",
|
|
"input_tokens" => 0,
|
|
"output_tokens" => 0,
|
|
"cost" => $image_detail_row["cost"] ?? 0,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
$detail_map = [];
|
|
foreach ($detail_rows as $detail_row) {
|
|
$pid = (int) ($detail_row["post_id"] ?? 0);
|
|
if (!isset($detail_map[$pid])) {
|
|
$detail_map[$pid] = [];
|
|
}
|
|
$detail_map[$pid][] = [
|
|
"created_at" => date_i18n(
|
|
"Y-m-d H:i:s",
|
|
strtotime($detail_row["created_at"]),
|
|
),
|
|
"model" => (string) ($detail_row["model"] ?? ""),
|
|
"action" => (string) ($detail_row["action"] ?? ""),
|
|
"input_tokens" => (int) ($detail_row["input_tokens"] ?? 0),
|
|
"output_tokens" =>
|
|
(int) ($detail_row["output_tokens"] ?? 0),
|
|
"cost" => number_format(
|
|
(float) ($detail_row["cost"] ?? 0),
|
|
4,
|
|
),
|
|
];
|
|
}
|
|
|
|
foreach ($formatted_records as $idx => $formatted_record) {
|
|
$pid = (int) ($formatted_record["post_id"] ?? 0);
|
|
$formatted_records[$idx]["details"] = $detail_map[$pid] ?? [];
|
|
$formatted_records[$idx]["details_total"] = count(
|
|
$formatted_records[$idx]["details"],
|
|
);
|
|
}
|
|
}
|
|
|
|
// Get summary stats (all-time aggregation in SQL)
|
|
$total_all_time = $wpdb->get_var(
|
|
"SELECT COALESCE(SUM(cost), 0) FROM {$table_name} WHERE provider = 'openrouter'",
|
|
);
|
|
$month_start = date("Y-m-01 00:00:00");
|
|
$monthly_total = $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"SELECT COALESCE(SUM(cost), 0) FROM {$table_name} WHERE provider = 'openrouter' AND created_at >= %s",
|
|
$month_start,
|
|
),
|
|
);
|
|
$today_total = $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"SELECT COALESCE(SUM(cost), 0) FROM {$table_name} WHERE provider = 'openrouter' AND DATE(created_at) = %s",
|
|
current_time("Y-m-d"),
|
|
),
|
|
);
|
|
$total_posts = $wpdb->get_var(
|
|
"SELECT COUNT(DISTINCT post_id) FROM {$table_name} WHERE provider = 'openrouter' AND post_id > 0",
|
|
);
|
|
$avg_per_post = $total_posts > 0 ? $total_all_time / $total_posts : 0;
|
|
|
|
$action_summary_rows = $wpdb->get_results(
|
|
"SELECT action, COUNT(*) AS calls, COALESCE(SUM(cost), 0) AS total_cost, COALESCE(AVG(cost), 0) AS avg_cost
|
|
FROM {$table_name}
|
|
WHERE provider = 'openrouter'
|
|
GROUP BY action
|
|
ORDER BY total_cost DESC",
|
|
ARRAY_A,
|
|
);
|
|
$action_summary = [];
|
|
foreach ($action_summary_rows as $row) {
|
|
$action_summary[] = [
|
|
"action" => (string) ($row["action"] ?? ""),
|
|
"calls" => (int) ($row["calls"] ?? 0),
|
|
"total" => number_format((float) ($row["total_cost"] ?? 0), 4),
|
|
"average" => number_format((float) ($row["avg_cost"] ?? 0), 4),
|
|
];
|
|
}
|
|
|
|
// Get filter options (distinct values from DB)
|
|
$models = $wpdb->get_col(
|
|
"SELECT DISTINCT model FROM {$table_name} WHERE provider = 'openrouter' ORDER BY model LIMIT 100",
|
|
);
|
|
$types = $wpdb->get_col(
|
|
"SELECT DISTINCT action FROM {$table_name} WHERE provider = 'openrouter' ORDER BY action",
|
|
);
|
|
|
|
wp_send_json_success([
|
|
"records" => $formatted_records,
|
|
"total_items" => intval($total_items),
|
|
"total_pages" => intval($total_pages),
|
|
"current_page" => $page,
|
|
"per_page" => $per_page,
|
|
"stats" => [
|
|
"all_time" => number_format((float) $total_all_time, 4),
|
|
"monthly" => number_format((float) $monthly_total, 4),
|
|
"today" => number_format((float) $today_total, 4),
|
|
"avg_per_post" => number_format((float) $avg_per_post, 4),
|
|
"action_summary" => $action_summary,
|
|
],
|
|
"filters" => [
|
|
"models" => $models,
|
|
"types" => $types,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for header statistics.
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
public function ajax_get_header_stats()
|
|
{
|
|
if (!check_ajax_referer("wpaw_settings", "nonce", false)) {
|
|
wp_send_json_error(["message" => "Invalid nonce"]);
|
|
return;
|
|
}
|
|
|
|
if (!current_user_can("manage_options")) {
|
|
wp_send_json_error(["message" => "Permission denied"]);
|
|
return;
|
|
}
|
|
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . "wpaw_cost_tracking";
|
|
|
|
// Check if table exists
|
|
$table_exists =
|
|
$wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") === $table_name;
|
|
|
|
if (!$table_exists) {
|
|
wp_send_json_success([
|
|
"articles" => 0,
|
|
"total_cost" => "0.00",
|
|
"api_status" => "Not configured",
|
|
"api_online" => false,
|
|
"last_updated" => "Never",
|
|
]);
|
|
return;
|
|
}
|
|
|
|
// Get total articles
|
|
$total_articles = $wpdb->get_var(
|
|
"SELECT COUNT(DISTINCT post_id) FROM {$table_name} WHERE post_id > 0",
|
|
);
|
|
|
|
// Get total cost
|
|
$total_cost = $wpdb->get_var("SELECT SUM(cost) FROM {$table_name}");
|
|
|
|
// Check API status
|
|
$settings = get_option("wp_agentic_writer_settings", []);
|
|
$api_key = $settings["openrouter_api_key"] ?? "";
|
|
$api_online = !empty($api_key);
|
|
|
|
// Get last activity
|
|
$last_activity = $wpdb->get_var(
|
|
"SELECT created_at FROM {$table_name} ORDER BY created_at DESC LIMIT 1",
|
|
);
|
|
$last_updated = $last_activity
|
|
? human_time_diff(
|
|
strtotime($last_activity),
|
|
current_time("timestamp"),
|
|
) . " ago"
|
|
: "Never";
|
|
|
|
wp_send_json_success([
|
|
"articles" => intval($total_articles),
|
|
"total_cost" => number_format((float) $total_cost, 2),
|
|
"api_status" => $api_online ? "Online" : "Not configured",
|
|
"api_online" => $api_online,
|
|
"last_updated" => $last_updated,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for debugging models.
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
public function ajax_debug_models()
|
|
{
|
|
if (!check_ajax_referer("wpaw_settings", "nonce", false)) {
|
|
wp_send_json_error(["message" => "Invalid nonce"]);
|
|
return;
|
|
}
|
|
|
|
if (!current_user_can("manage_options")) {
|
|
wp_send_json_error(["message" => "Permission denied"]);
|
|
return;
|
|
}
|
|
|
|
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
|
|
$models = $provider->get_cached_models();
|
|
|
|
if (is_wp_error($models)) {
|
|
wp_send_json_error(["message" => $models->get_error_message()]);
|
|
return;
|
|
}
|
|
|
|
// Check for specific models
|
|
$check_models = [
|
|
"deepseek/deepseek-chat-v3-0324",
|
|
"anthropic/claude-sonnet-4",
|
|
];
|
|
$found_models = [];
|
|
$missing_models = [];
|
|
|
|
foreach ($check_models as $check_id) {
|
|
$found = false;
|
|
foreach ($models as $model) {
|
|
if (isset($model["id"]) && $model["id"] === $check_id) {
|
|
$found = true;
|
|
$found_models[] = [
|
|
"id" => $model["id"],
|
|
"name" => $model["name"] ?? "N/A",
|
|
];
|
|
break;
|
|
}
|
|
}
|
|
if (!$found) {
|
|
$missing_models[] = $check_id;
|
|
}
|
|
}
|
|
|
|
wp_send_json_success([
|
|
"total_models" => count($models),
|
|
"found_models" => $found_models,
|
|
"missing_models" => $missing_models,
|
|
"sample_models" => array_slice(
|
|
array_map(function ($m) {
|
|
return [
|
|
"id" => $m["id"] ?? "N/A",
|
|
"name" => $m["name"] ?? "N/A",
|
|
];
|
|
}, $models),
|
|
0,
|
|
10,
|
|
),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for testing API connection.
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
public function ajax_test_api_connection()
|
|
{
|
|
if (!check_ajax_referer("wpaw_settings", "nonce", false)) {
|
|
wp_send_json_error(["message" => "Invalid nonce"]);
|
|
return;
|
|
}
|
|
|
|
if (!current_user_can("manage_options")) {
|
|
wp_send_json_error(["message" => "Permission denied"]);
|
|
return;
|
|
}
|
|
|
|
$settings = get_option("wp_agentic_writer_settings", []);
|
|
$posted_api_key = isset($_POST["api_key"])
|
|
? trim(sanitize_text_field(wp_unslash($_POST["api_key"])))
|
|
: "";
|
|
$api_key = !empty($posted_api_key)
|
|
? $posted_api_key
|
|
: $settings["openrouter_api_key"] ?? "";
|
|
|
|
if (empty($api_key)) {
|
|
wp_send_json_error(["message" => "API key is not configured"]);
|
|
return;
|
|
}
|
|
|
|
// Test API connection by making a simple request
|
|
$response = wp_remote_get(
|
|
"https://openrouter.ai/api/v1/models?output_modalities=all",
|
|
[
|
|
"headers" => [
|
|
"Authorization" => "Bearer " . $api_key,
|
|
"HTTP-Referer" => home_url(),
|
|
],
|
|
"timeout" => 10,
|
|
],
|
|
);
|
|
|
|
if (is_wp_error($response)) {
|
|
wp_send_json_error([
|
|
"message" =>
|
|
"Connection failed: " . $response->get_error_message(),
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$status_code = wp_remote_retrieve_response_code($response);
|
|
$body = wp_remote_retrieve_body($response);
|
|
|
|
if (200 === $status_code) {
|
|
$data = json_decode($body, true);
|
|
if (isset($data["data"]) && is_array($data["data"])) {
|
|
wp_send_json_success([
|
|
"message" => "API connection successful!",
|
|
"models_count" => count($data["data"]),
|
|
]);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Handle error responses
|
|
if (401 === $status_code) {
|
|
wp_send_json_error(["message" => "Invalid API key"]);
|
|
return;
|
|
}
|
|
|
|
if (403 === $status_code) {
|
|
wp_send_json_error([
|
|
"message" =>
|
|
"Access forbidden - check your API key permissions",
|
|
]);
|
|
return;
|
|
}
|
|
|
|
wp_send_json_error([
|
|
"message" =>
|
|
"API connection failed with status code: " . $status_code,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Add settings page to admin menu.
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
public function add_settings_page()
|
|
{
|
|
add_options_page(
|
|
__("WP Agentic Writer", "wp-agentic-writer"),
|
|
__("Agentic Writer", "wp-agentic-writer"),
|
|
"manage_options",
|
|
"wp-agentic-writer-settings",
|
|
[$this, "render_settings_page"],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Register settings.
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
public function register_settings()
|
|
{
|
|
register_setting(
|
|
"wp_agentic_writer_settings",
|
|
"wp_agentic_writer_settings",
|
|
[
|
|
"sanitize_callback" => [$this, "sanitize_settings"],
|
|
],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sanitize settings.
|
|
*
|
|
* @since 0.2.0
|
|
* @param array $input Settings input.
|
|
* @return array Sanitized settings.
|
|
*/
|
|
public function sanitize_settings($input)
|
|
{
|
|
$sanitized = [];
|
|
|
|
// Sanitize API keys (allow empty values to clear them)
|
|
if (isset($input["openrouter_api_key"])) {
|
|
$sanitized["openrouter_api_key"] = trim(
|
|
$input["openrouter_api_key"],
|
|
);
|
|
}
|
|
if (isset($input["brave_search_api_key"])) {
|
|
$sanitized["brave_search_api_key"] = trim(
|
|
$input["brave_search_api_key"],
|
|
);
|
|
}
|
|
|
|
if (isset($input["custom_search_url"])) {
|
|
$sanitized["custom_search_url"] = esc_url_raw(
|
|
trim($input["custom_search_url"]),
|
|
);
|
|
}
|
|
|
|
// Sanitize model names (6 models) - using model registry for defaults
|
|
$sanitized["chat_model"] = sanitize_text_field(
|
|
$input["chat_model"] ??
|
|
WPAW_Model_Registry::get_default_model("chat"),
|
|
);
|
|
$sanitized["clarity_model"] = sanitize_text_field(
|
|
$input["clarity_model"] ??
|
|
WPAW_Model_Registry::get_default_model("clarity"),
|
|
);
|
|
$sanitized["planning_model"] = sanitize_text_field(
|
|
$input["planning_model"] ??
|
|
WPAW_Model_Registry::get_default_model("planning"),
|
|
);
|
|
$sanitized["writing_model"] = sanitize_text_field(
|
|
$input["writing_model"] ??
|
|
WPAW_Model_Registry::get_default_model("writing"),
|
|
);
|
|
$sanitized["refinement_model"] = sanitize_text_field(
|
|
$input["refinement_model"] ??
|
|
WPAW_Model_Registry::get_default_model("refinement"),
|
|
);
|
|
$sanitized["image_model"] = sanitize_text_field(
|
|
$input["image_model"] ??
|
|
WPAW_Model_Registry::get_default_model("image"),
|
|
);
|
|
|
|
// Legacy support: map execution_model to writing_model
|
|
if (
|
|
isset($input["execution_model"]) &&
|
|
!isset($input["writing_model"])
|
|
) {
|
|
$sanitized["writing_model"] = sanitize_text_field(
|
|
$input["execution_model"],
|
|
);
|
|
}
|
|
|
|
// Sanitize boolean values
|
|
$sanitized["web_search_enabled"] =
|
|
isset($input["web_search_enabled"]) &&
|
|
"1" === $input["web_search_enabled"];
|
|
$sanitized["cost_tracking_enabled"] =
|
|
isset($input["cost_tracking_enabled"]) &&
|
|
"1" === $input["cost_tracking_enabled"];
|
|
$sanitized["enable_clarification_quiz"] =
|
|
isset($input["enable_clarification_quiz"]) &&
|
|
"1" === $input["enable_clarification_quiz"];
|
|
$sanitized["enable_faq_schema"] = isset($input["enable_faq_schema"])
|
|
? "1" === $input["enable_faq_schema"]
|
|
: false;
|
|
$sanitized["allow_openrouter_fallback"] =
|
|
isset($input["allow_openrouter_fallback"]) &&
|
|
"1" === $input["allow_openrouter_fallback"];
|
|
$sanitized["openrouter_provider_routing_enabled"] =
|
|
isset($input["openrouter_provider_routing_enabled"]) &&
|
|
"1" === $input["openrouter_provider_routing_enabled"];
|
|
$sanitized["openrouter_provider_only"] =
|
|
isset($input["openrouter_provider_only"]) &&
|
|
"1" === $input["openrouter_provider_only"];
|
|
$sanitized["openrouter_allow_provider_fallbacks"] =
|
|
isset($input["openrouter_allow_provider_fallbacks"]) &&
|
|
"1" === $input["openrouter_allow_provider_fallbacks"];
|
|
|
|
$provider_slug = isset($input["openrouter_provider_slug"])
|
|
? sanitize_key($input["openrouter_provider_slug"])
|
|
: "auto";
|
|
$sanitized["openrouter_provider_slug"] =
|
|
"" !== $provider_slug ? $provider_slug : "auto";
|
|
|
|
// Sanitize search options
|
|
$sanitized["search_engine"] = in_array(
|
|
$input["search_engine"] ?? "",
|
|
["auto", "native", "exa"],
|
|
true,
|
|
)
|
|
? $input["search_engine"]
|
|
: "auto";
|
|
$sanitized["search_depth"] =
|
|
isset($input["search_depth"]) &&
|
|
in_array($input["search_depth"], ["low", "medium", "high"], true)
|
|
? $input["search_depth"]
|
|
: "medium";
|
|
|
|
// Sanitize budget
|
|
$sanitized["monthly_budget"] = floatval(
|
|
$input["monthly_budget"] ?? 600,
|
|
);
|
|
|
|
// Sanitize chat history limit
|
|
$chat_history_limit = isset($input["chat_history_limit"])
|
|
? absint($input["chat_history_limit"])
|
|
: 20;
|
|
$sanitized["chat_history_limit"] = min($chat_history_limit, 200);
|
|
|
|
// Sanitize clarification quiz settings
|
|
$sanitized["clarity_confidence_threshold"] = in_array(
|
|
$input["clarity_confidence_threshold"] ?? "",
|
|
["0.5", "0.6", "0.7", "0.8", "0.9"],
|
|
true,
|
|
)
|
|
? $input["clarity_confidence_threshold"]
|
|
: "0.6";
|
|
|
|
if (
|
|
isset($input["required_context_categories"]) &&
|
|
is_array($input["required_context_categories"])
|
|
) {
|
|
$valid_categories = [
|
|
"target_outcome",
|
|
"target_audience",
|
|
"tone",
|
|
"content_depth",
|
|
"expertise_level",
|
|
"content_type",
|
|
"pov",
|
|
];
|
|
$sanitized["required_context_categories"] = array_intersect(
|
|
$input["required_context_categories"],
|
|
$valid_categories,
|
|
);
|
|
} else {
|
|
$sanitized["required_context_categories"] = [
|
|
"target_outcome",
|
|
"target_audience",
|
|
"tone",
|
|
"content_depth",
|
|
"expertise_level",
|
|
"content_type",
|
|
"pov",
|
|
];
|
|
}
|
|
|
|
// Sanitize preferred languages
|
|
if (
|
|
isset($input["preferred_languages"]) &&
|
|
is_array($input["preferred_languages"])
|
|
) {
|
|
$sanitized["preferred_languages"] = array_map(
|
|
"sanitize_text_field",
|
|
$input["preferred_languages"],
|
|
);
|
|
} else {
|
|
$sanitized["preferred_languages"] = [
|
|
"auto",
|
|
"English",
|
|
"Indonesian",
|
|
];
|
|
}
|
|
|
|
// Sanitize custom languages
|
|
if (
|
|
isset($input["custom_languages"]) &&
|
|
is_array($input["custom_languages"])
|
|
) {
|
|
$custom_language_values = [];
|
|
foreach ($input["custom_languages"] as $custom_language_value) {
|
|
$custom_language_values = array_merge(
|
|
$custom_language_values,
|
|
array_map(
|
|
"trim",
|
|
explode(",", (string) $custom_language_value),
|
|
),
|
|
);
|
|
}
|
|
$sanitized["custom_languages"] = array_filter(
|
|
array_map("sanitize_text_field", $custom_language_values),
|
|
);
|
|
} else {
|
|
$sanitized["custom_languages"] = [];
|
|
}
|
|
|
|
// Sanitize Global Context
|
|
$sanitized["global_context"] = isset($input["global_context"])
|
|
? sanitize_textarea_field(trim($input["global_context"]))
|
|
: "";
|
|
|
|
// Sanitize Custom Endpoint settings (Fix for settings wiping out)
|
|
if (isset($input["local_backend_url"])) {
|
|
$sanitized["local_backend_url"] = esc_url_raw(
|
|
trim($input["local_backend_url"]),
|
|
);
|
|
}
|
|
|
|
if (isset($input["local_backend_key"])) {
|
|
$sanitized["local_backend_key"] = sanitize_text_field(
|
|
trim($input["local_backend_key"]),
|
|
);
|
|
}
|
|
|
|
if (isset($input["local_backend_image_url"])) {
|
|
$sanitized["local_backend_image_url"] = esc_url_raw(
|
|
trim($input["local_backend_image_url"]),
|
|
);
|
|
}
|
|
|
|
if (isset($input["local_backend_image_key"])) {
|
|
$sanitized["local_backend_image_key"] = sanitize_text_field(
|
|
trim($input["local_backend_image_key"]),
|
|
);
|
|
}
|
|
|
|
if (isset($input["local_backend_model"])) {
|
|
$sanitized["local_backend_model"] = sanitize_text_field(
|
|
trim($input["local_backend_model"]),
|
|
);
|
|
}
|
|
|
|
$sanitized["local_backend_enabled"] =
|
|
isset($input["local_backend_enabled"]) &&
|
|
"1" === $input["local_backend_enabled"];
|
|
|
|
$sanitized["local_backend_image_enabled"] =
|
|
isset($input["local_backend_image_enabled"]) &&
|
|
"1" === $input["local_backend_image_enabled"];
|
|
|
|
// Per-task model overrides for custom endpoint
|
|
if (
|
|
isset($input["local_backend_models"]) &&
|
|
is_array($input["local_backend_models"])
|
|
) {
|
|
$sanitized_models = [];
|
|
$allowed_model_tasks = [
|
|
"chat",
|
|
"clarity",
|
|
"planning",
|
|
"writing",
|
|
"refinement",
|
|
"image",
|
|
];
|
|
foreach ($input["local_backend_models"] as $task => $model_code) {
|
|
$task = sanitize_text_field($task);
|
|
if (in_array($task, $allowed_model_tasks, true)) {
|
|
$sanitized_models[$task] = sanitize_text_field(
|
|
trim($model_code),
|
|
);
|
|
}
|
|
}
|
|
$sanitized["local_backend_models"] = $sanitized_models;
|
|
}
|
|
|
|
// Sanitize MEMANTO settings.
|
|
$sanitized["memanto_enabled"] =
|
|
isset($input["memanto_enabled"]) &&
|
|
"1" === $input["memanto_enabled"];
|
|
$sanitized["memanto_url"] = isset($input["memanto_url"])
|
|
? esc_url_raw(trim($input["memanto_url"]))
|
|
: "";
|
|
$sanitized["memanto_license_key"] = isset($input["memanto_license_key"])
|
|
? sanitize_text_field(trim($input["memanto_license_key"]))
|
|
: "";
|
|
$sanitized["memanto_moorcheh_key"] = isset(
|
|
$input["memanto_moorcheh_key"],
|
|
)
|
|
? sanitize_text_field(trim($input["memanto_moorcheh_key"]))
|
|
: "";
|
|
|
|
// Sanitize Task Providers Routing.
|
|
// When custom endpoint is enabled, auto-build task_providers from
|
|
// the per-task model codes: a non-empty model code ⇒ local_backend.
|
|
if (!empty($sanitized["local_backend_enabled"])) {
|
|
$sanitized_providers = [];
|
|
$lb_models = $sanitized["local_backend_models"] ?? [];
|
|
$all_tasks = [
|
|
"chat",
|
|
"clarity",
|
|
"planning",
|
|
"writing",
|
|
"refinement",
|
|
"image",
|
|
];
|
|
foreach ($all_tasks as $task) {
|
|
if ($task === "image") {
|
|
$sanitized_providers[$task] = !empty(
|
|
$sanitized["local_backend_image_enabled"]
|
|
)
|
|
? "local_backend"
|
|
: "openrouter";
|
|
} else {
|
|
$sanitized_providers[$task] = !empty($lb_models[$task])
|
|
? "local_backend"
|
|
: "openrouter";
|
|
}
|
|
}
|
|
$sanitized["task_providers"] = $sanitized_providers;
|
|
} elseif (
|
|
isset($input["task_providers"]) &&
|
|
is_array($input["task_providers"])
|
|
) {
|
|
$sanitized_providers = [];
|
|
$allowed_tasks = [
|
|
"chat",
|
|
"clarity",
|
|
"planning",
|
|
"writing",
|
|
"refinement",
|
|
"image",
|
|
];
|
|
$allowed_providers_text = ["openrouter", "local_backend", "codex"];
|
|
|
|
foreach ($input["task_providers"] as $task => $provider) {
|
|
$task = sanitize_text_field($task);
|
|
$provider = sanitize_text_field($provider);
|
|
|
|
if (in_array($task, $allowed_tasks, true)) {
|
|
if (in_array($provider, $allowed_providers_text, true)) {
|
|
$sanitized_providers[$task] = $provider;
|
|
}
|
|
}
|
|
}
|
|
$sanitized["task_providers"] = $sanitized_providers;
|
|
}
|
|
|
|
return $sanitized;
|
|
}
|
|
|
|
/**
|
|
* Render settings page - main entry point.
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
public function render_settings_page()
|
|
{
|
|
$settings = get_option("wp_agentic_writer_settings", []);
|
|
|
|
// Extract settings for views
|
|
$view_data = $this->prepare_view_data($settings);
|
|
|
|
// Include Stitch rebuild layout
|
|
include WP_AGENTIC_WRITER_DIR . "views/settings-v2/layout.php";
|
|
}
|
|
|
|
/**
|
|
* Prepare data for view files.
|
|
*
|
|
* @since 0.2.0
|
|
* @param array $settings Plugin settings.
|
|
* @return array View data.
|
|
*/
|
|
private function prepare_view_data($settings)
|
|
{
|
|
// Extract settings (6 models) using model registry for defaults
|
|
$api_key = $settings["openrouter_api_key"] ?? "";
|
|
$brave_search_api_key = $settings["brave_search_api_key"] ?? "";
|
|
$custom_search_url = $settings["custom_search_url"] ?? "";
|
|
$chat_model =
|
|
$settings["chat_model"] ??
|
|
WPAW_Model_Registry::get_default_model("chat");
|
|
$clarity_model =
|
|
$settings["clarity_model"] ??
|
|
WPAW_Model_Registry::get_default_model("clarity");
|
|
$planning_model =
|
|
$settings["planning_model"] ??
|
|
WPAW_Model_Registry::get_default_model("planning");
|
|
$writing_model =
|
|
$settings["writing_model"] ??
|
|
($settings["execution_model"] ??
|
|
WPAW_Model_Registry::get_default_model("writing"));
|
|
$refinement_model =
|
|
$settings["refinement_model"] ??
|
|
WPAW_Model_Registry::get_default_model("refinement");
|
|
$image_model =
|
|
$settings["image_model"] ??
|
|
WPAW_Model_Registry::get_default_model("image");
|
|
$web_search_enabled = $settings["web_search_enabled"] ?? false;
|
|
$search_engine = $settings["search_engine"] ?? "auto";
|
|
$search_depth = $settings["search_depth"] ?? "medium";
|
|
$cost_tracking_enabled = $settings["cost_tracking_enabled"] ?? true;
|
|
$monthly_budget = $settings["monthly_budget"] ?? 600;
|
|
$chat_history_limit = $settings["chat_history_limit"] ?? 20;
|
|
$enable_clarification_quiz =
|
|
$settings["enable_clarification_quiz"] ?? true;
|
|
$enable_faq_schema = $settings["enable_faq_schema"] ?? false;
|
|
$clarity_confidence_threshold =
|
|
$settings["clarity_confidence_threshold"] ?? "0.6";
|
|
$required_context_categories = $settings[
|
|
"required_context_categories"
|
|
] ?? [
|
|
"target_outcome",
|
|
"target_audience",
|
|
"tone",
|
|
"content_depth",
|
|
"expertise_level",
|
|
"content_type",
|
|
"pov",
|
|
];
|
|
$preferred_languages = $settings["preferred_languages"] ?? [
|
|
"auto",
|
|
"English",
|
|
"Indonesian",
|
|
];
|
|
$custom_languages = $settings["custom_languages"] ?? [];
|
|
$global_context = $settings["global_context"] ?? "";
|
|
$available_languages = $this->get_available_languages();
|
|
$custom_models = get_option("wp_agentic_writer_custom_models", []);
|
|
|
|
// Custom Endpoint settings
|
|
$local_backend_url = $settings["local_backend_url"] ?? "";
|
|
$local_backend_key = $settings["local_backend_key"] ?? "";
|
|
$local_backend_image_url = $settings["local_backend_image_url"] ?? "";
|
|
$local_backend_image_key = $settings["local_backend_image_key"] ?? "";
|
|
$local_backend_model = $settings["local_backend_model"] ?? "";
|
|
$local_backend_enabled = !empty($settings["local_backend_enabled"]);
|
|
$local_backend_image_enabled = !empty(
|
|
$settings["local_backend_image_enabled"]
|
|
);
|
|
$local_backend_models = $settings["local_backend_models"] ?? [];
|
|
|
|
// MEMANTO settings
|
|
$memanto_enabled = $settings["memanto_enabled"] ?? false;
|
|
$memanto_url = $settings["memanto_url"] ?? "";
|
|
$memanto_license_key = $settings["memanto_license_key"] ?? "";
|
|
$memanto_moorcheh_key = $settings["memanto_moorcheh_key"] ?? "";
|
|
$task_providers = $settings["task_providers"] ?? [];
|
|
$allow_openrouter_fallback = !empty(
|
|
$settings["allow_openrouter_fallback"]
|
|
);
|
|
$openrouter_provider_routing_enabled = !empty(
|
|
$settings["openrouter_provider_routing_enabled"]
|
|
);
|
|
$openrouter_provider_slug =
|
|
$settings["openrouter_provider_slug"] ?? "auto";
|
|
$openrouter_provider_only = !empty(
|
|
$settings["openrouter_provider_only"]
|
|
);
|
|
$openrouter_allow_provider_fallbacks = !empty(
|
|
$settings["openrouter_allow_provider_fallbacks"]
|
|
);
|
|
|
|
// Get cost tracking data
|
|
$cost_tracker = WP_Agentic_Writer_Cost_Tracker::get_instance();
|
|
$monthly_used = $cost_tracker->get_monthly_total();
|
|
$budget_percent =
|
|
$monthly_budget > 0 ? ($monthly_used / $monthly_budget) * 100 : 0;
|
|
$budget_status =
|
|
$budget_percent > 90
|
|
? "danger"
|
|
: ($budget_percent > 70
|
|
? "warning"
|
|
: "success");
|
|
|
|
return compact(
|
|
"api_key",
|
|
"brave_search_api_key",
|
|
"custom_search_url",
|
|
"chat_model",
|
|
"clarity_model",
|
|
"planning_model",
|
|
"writing_model",
|
|
"refinement_model",
|
|
"image_model",
|
|
"web_search_enabled",
|
|
"search_engine",
|
|
"search_depth",
|
|
"cost_tracking_enabled",
|
|
"monthly_budget",
|
|
"chat_history_limit",
|
|
"enable_clarification_quiz",
|
|
"enable_faq_schema",
|
|
"clarity_confidence_threshold",
|
|
"required_context_categories",
|
|
"preferred_languages",
|
|
"custom_languages",
|
|
"global_context",
|
|
"available_languages",
|
|
"custom_models",
|
|
"monthly_used",
|
|
"budget_percent",
|
|
"budget_status",
|
|
"local_backend_url",
|
|
"local_backend_key",
|
|
"local_backend_image_url",
|
|
"local_backend_image_key",
|
|
"local_backend_model",
|
|
"local_backend_enabled",
|
|
"local_backend_image_enabled",
|
|
"local_backend_models",
|
|
"task_providers",
|
|
"allow_openrouter_fallback",
|
|
"openrouter_provider_routing_enabled",
|
|
"openrouter_provider_slug",
|
|
"openrouter_provider_only",
|
|
"openrouter_allow_provider_fallbacks",
|
|
"memanto_enabled",
|
|
"memanto_url",
|
|
"memanto_license_key",
|
|
"memanto_moorcheh_key",
|
|
"settings",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get available languages list.
|
|
*
|
|
* @since 0.2.0
|
|
* @return array Available languages.
|
|
*/
|
|
public function get_available_languages()
|
|
{
|
|
return [
|
|
"auto" => "Auto-detect",
|
|
"English" => "English",
|
|
"Indonesian" => "Indonesian (Bahasa Indonesia)",
|
|
"Javanese" => "Javanese (Basa Jawa)",
|
|
"Sundanese" => "Sundanese (Basa Sunda)",
|
|
"Spanish" => "Spanish (Español)",
|
|
"French" => "French (Français)",
|
|
"Arabic" => "Arabic (العربية)",
|
|
"Chinese" => "Chinese (中文)",
|
|
"Japanese" => "Japanese (日本語)",
|
|
"Portuguese" => "Portuguese (Português)",
|
|
"German" => "German (Deutsch)",
|
|
"Hindi" => "Hindi (हिंदी)",
|
|
"Korean" => "Korean (한국어)",
|
|
"Vietnamese" => "Vietnamese (Tiếng Việt)",
|
|
"Thai" => "Thai (ไทย)",
|
|
"Tagalog" => "Tagalog",
|
|
"Malay" => "Malay (Bahasa Melayu)",
|
|
"Russian" => "Russian (Русский)",
|
|
"Italian" => "Italian (Italiano)",
|
|
"Dutch" => "Dutch (Nederlands)",
|
|
"Polish" => "Polish (Polski)",
|
|
"Turkish" => "Turkish (Türkçe)",
|
|
"Swedish" => "Swedish (Svenska)",
|
|
];
|
|
}
|
|
|
|
/**
|
|
* AJAX handler: Test local backend connection
|
|
*
|
|
* @since 0.2.0
|
|
*/
|
|
public function ajax_test_local_backend()
|
|
{
|
|
check_ajax_referer("wpaw_settings", "nonce");
|
|
|
|
if (!current_user_can("manage_options")) {
|
|
wp_send_json_error(["message" => "Insufficient permissions"]);
|
|
}
|
|
|
|
$url = sanitize_text_field(wp_unslash($_POST["url"] ?? ""));
|
|
$key = sanitize_text_field(wp_unslash($_POST["key"] ?? ""));
|
|
$model = sanitize_text_field(wp_unslash($_POST["model"] ?? ""));
|
|
|
|
if (empty($url)) {
|
|
wp_send_json_error(["message" => "URL required"]);
|
|
}
|
|
|
|
// Temporarily create provider with these form values.
|
|
$original_settings = get_option("wp_agentic_writer_settings", []);
|
|
$temp_settings = $original_settings;
|
|
$temp_settings["local_backend_url"] = $url;
|
|
if ("" !== $key) {
|
|
$temp_settings["local_backend_key"] = $key;
|
|
}
|
|
if ("" !== $model) {
|
|
$temp_settings["local_backend_model"] = $model;
|
|
}
|
|
update_option("wp_agentic_writer_settings", $temp_settings);
|
|
|
|
$provider = new WP_Agentic_Writer_Local_Backend_Provider();
|
|
$result = $provider->test_connection();
|
|
|
|
update_option("wp_agentic_writer_settings", $original_settings);
|
|
|
|
if (is_wp_error($result)) {
|
|
wp_send_json_error(["message" => $result->get_error_message()]);
|
|
}
|
|
|
|
wp_send_json_success($result);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler: Test MEMANTO connection.
|
|
*
|
|
* Uses the provided URL and key (from the form) to test connectivity
|
|
* without requiring settings to be saved first.
|
|
*
|
|
* @since 0.3.0
|
|
*/
|
|
public function ajax_test_memanto()
|
|
{
|
|
check_ajax_referer("wpaw_settings", "nonce");
|
|
|
|
if (!current_user_can("manage_options")) {
|
|
wp_send_json_error(["message" => "Insufficient permissions"]);
|
|
}
|
|
|
|
$url = sanitize_text_field(wp_unslash($_POST["url"] ?? ""));
|
|
$key = sanitize_text_field(wp_unslash($_POST["key"] ?? ""));
|
|
|
|
if (empty($url) || empty($key)) {
|
|
wp_send_json_error([
|
|
"message" => "MEMANTO URL and Moorcheh API key are required.",
|
|
]);
|
|
}
|
|
|
|
// Temporarily override settings so the client uses the form values.
|
|
$original_settings = get_option("wp_agentic_writer_settings", []);
|
|
$temp_settings = $original_settings;
|
|
$temp_settings["memanto_url"] = esc_url_raw(trim($url));
|
|
$temp_settings["memanto_moorcheh_key"] = sanitize_text_field(
|
|
trim($key),
|
|
);
|
|
update_option("wp_agentic_writer_settings", $temp_settings);
|
|
|
|
// Clear health cache so the fresh URL/key are used.
|
|
delete_transient("wpaw_memanto_health");
|
|
|
|
$client = WP_Agentic_Writer_Memanto_Client::for_base_url(
|
|
esc_url_raw(trim($url)),
|
|
);
|
|
$result = $client->check_health_fresh();
|
|
update_option("wp_agentic_writer_settings", $original_settings);
|
|
delete_transient("wpaw_memanto_health");
|
|
|
|
if ($result["healthy"]) {
|
|
wp_send_json_success($result);
|
|
} else {
|
|
$error_msg = "Connection failed.";
|
|
if (!empty($result["details"]["error"])) {
|
|
$error_msg = $result["details"]["error"];
|
|
}
|
|
wp_send_json_error(["message" => $error_msg, "healthy" => false]);
|
|
}
|
|
}
|
|
}
|