feat: Add connection test caching and reasoning content support

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
This commit is contained in:
Dwindi Ramadhana
2026-06-17 05:26:12 +07:00
parent f55acd7d26
commit d3f142222c
4 changed files with 1765 additions and 516 deletions

View File

@@ -2,7 +2,7 @@
/**
* Settings Page V2
*
* Refactored settings page with Bootstrap design and separated view files.
* Refactored settings page with Agentic design tokens and separated view files.
*
* @package WP_Agentic_Writer
*/
@@ -75,6 +75,93 @@ class WP_Agentic_Writer_Settings_V2
"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.",
);
}
}
/**
@@ -89,115 +176,36 @@ class WP_Agentic_Writer_Settings_V2
return;
}
// Bootstrap 5.3
$settings_css_path =
WP_AGENTIC_WRITER_DIR . "views/settings-v2/style.css";
wp_enqueue_style(
"bootstrap",
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css",
"wpaw-settings-v2-stitch",
WP_AGENTIC_WRITER_URL . "views/settings-v2/style.css",
[],
"5.3.3",
);
wp_enqueue_style(
"bootstrap-icons",
"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css",
[],
"1.11.1",
);
wp_enqueue_script(
"bootstrap",
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js",
[],
"5.3.3",
true,
file_exists($settings_css_path)
? filemtime($settings_css_path)
: WP_AGENTIC_WRITER_VERSION,
);
// Select2 for searchable dropdowns
wp_enqueue_style(
"select2",
"https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css",
"wpaw-select2",
WP_AGENTIC_WRITER_URL . "assets/vendor/select2/select2.min.css",
[],
"4.1.0",
);
wp_enqueue_style(
"select2-bootstrap-5",
"https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css",
["select2", "bootstrap"],
"1.3.0",
"4.1.0-rc.0",
);
wp_enqueue_script(
"select2",
"https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js",
"wpaw-select2",
WP_AGENTIC_WRITER_URL . "assets/vendor/select2/select2.min.js",
["jquery"],
"4.1.0",
"4.1.0-rc.0",
true,
);
// Agentic Vibe CSS - Design System (in order)
wp_enqueue_style(
"wpaw-agentic-variables",
WP_AGENTIC_WRITER_URL . "assets/css/agentic-variables.css",
[],
WP_AGENTIC_WRITER_VERSION,
);
wp_enqueue_style(
"wpaw-agentic-bootstrap-custom",
WP_AGENTIC_WRITER_URL . "assets/css/agentic-bootstrap-custom.css",
["bootstrap", "wpaw-agentic-variables"],
WP_AGENTIC_WRITER_VERSION,
);
wp_enqueue_style(
"wpaw-agentic-components",
WP_AGENTIC_WRITER_URL . "assets/css/agentic-components.css",
["wpaw-agentic-variables"],
WP_AGENTIC_WRITER_VERSION,
);
wp_enqueue_style(
"wpaw-agentic-workflow",
WP_AGENTIC_WRITER_URL . "assets/css/agentic-workflow.css",
["wpaw-agentic-components"],
WP_AGENTIC_WRITER_VERSION,
);
// Legacy plugin styles
$css_admin_path = WP_AGENTIC_WRITER_DIR . "assets/css/admin-v2.css";
$css_settings_path =
WP_AGENTIC_WRITER_DIR . "assets/css/settings-v2.css";
$css_log_path =
WP_AGENTIC_WRITER_DIR . "assets/css/cost-log-grouped.css";
$ver_admin = file_exists($css_admin_path)
? filemtime($css_admin_path)
: WP_AGENTIC_WRITER_VERSION;
$ver_settings = file_exists($css_settings_path)
? filemtime($css_settings_path)
: WP_AGENTIC_WRITER_VERSION;
$ver_log = file_exists($css_log_path)
? filemtime($css_log_path)
: WP_AGENTIC_WRITER_VERSION;
wp_enqueue_style(
"wp-agentic-writer-admin-v2",
WP_AGENTIC_WRITER_URL . "assets/css/admin-v2.css",
["bootstrap", "select2-bootstrap-5"],
$ver_admin,
);
wp_enqueue_style(
"wp-agentic-writer-settings-v2",
WP_AGENTIC_WRITER_URL . "assets/css/settings-v2.css",
["wpaw-agentic-components"],
$ver_settings,
);
wp_enqueue_style(
"wp-agentic-writer-cost-log-grouped",
WP_AGENTIC_WRITER_URL . "assets/css/cost-log-grouped.css",
["wp-agentic-writer-settings-v2"],
$ver_log,
);
// Plugin scripts
wp_enqueue_script(
"wp-agentic-writer-settings-v2",
WP_AGENTIC_WRITER_URL . "assets/js/settings-v2.js",
["jquery", "bootstrap", "select2"],
WP_AGENTIC_WRITER_URL . "assets/js/settings-v2-stitch.js",
["jquery", "wpaw-select2"],
WP_AGENTIC_WRITER_VERSION,
true,
);
@@ -282,8 +290,8 @@ class WP_Agentic_Writer_Settings_V2
"chat" => "google/gemini-2.5-flash",
"clarity" => "google/gemini-2.5-flash",
"planning" => "google/gemini-2.5-flash",
"writing" => "anthropic/claude-3.5-sonnet",
"refinement" => "anthropic/claude-3.5-sonnet",
"writing" => "anthropic/claude-sonnet-4",
"refinement" => "anthropic/claude-sonnet-4",
"image" => "openai/gpt-4o",
],
"premium" => [
@@ -704,7 +712,17 @@ class WP_Agentic_Writer_Settings_V2
wp_send_json_error(["message" => "Permission denied"]);
}
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
$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)) {
@@ -1025,6 +1043,58 @@ class WP_Agentic_Writer_Settings_V2
...$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);
@@ -1221,7 +1291,7 @@ class WP_Agentic_Writer_Settings_V2
// Check for specific models
$check_models = [
"deepseek/deepseek-chat-v3-0324",
"anthropic/claude-3.5-sonnet",
"anthropic/claude-sonnet-4",
];
$found_models = [];
$missing_models = [];
@@ -1278,7 +1348,12 @@ class WP_Agentic_Writer_Settings_V2
}
$settings = get_option("wp_agentic_writer_settings", []);
$api_key = $settings["openrouter_api_key"] ?? "";
$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"]);
@@ -1394,6 +1469,12 @@ class WP_Agentic_Writer_Settings_V2
);
}
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"] ??
@@ -1547,14 +1628,29 @@ class WP_Agentic_Writer_Settings_V2
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", $input["custom_languages"]),
array_map("sanitize_text_field", $custom_language_values),
);
} else {
$sanitized["custom_languages"] = [];
}
// Sanitize Local Backend settings (Fix for settings wiping out)
// 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"]),
@@ -1567,12 +1663,57 @@ class WP_Agentic_Writer_Settings_V2
);
}
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"]) &&
@@ -1580,14 +1721,44 @@ class WP_Agentic_Writer_Settings_V2
$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
if (
// 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"])
) {
@@ -1607,12 +1778,7 @@ class WP_Agentic_Writer_Settings_V2
$provider = sanitize_text_field($provider);
if (in_array($task, $allowed_tasks, true)) {
if ("image" === $task && "openrouter" === $provider) {
$sanitized_providers[$task] = $provider;
} elseif (
"image" !== $task &&
in_array($provider, $allowed_providers_text, true)
) {
if (in_array($provider, $allowed_providers_text, true)) {
$sanitized_providers[$task] = $provider;
}
}
@@ -1635,8 +1801,8 @@ class WP_Agentic_Writer_Settings_V2
// Extract settings for views
$view_data = $this->prepare_view_data($settings);
// Include main layout
include WP_AGENTIC_WRITER_DIR . "views/settings/layout.php";
// Include Stitch rebuild layout
include WP_AGENTIC_WRITER_DIR . "views/settings-v2/layout.php";
}
/**
@@ -1651,6 +1817,7 @@ class WP_Agentic_Writer_Settings_V2
// 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");
@@ -1698,17 +1865,26 @@ class WP_Agentic_Writer_Settings_V2
"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", []);
// Local Backend settings
// Custom Endpoint settings
$local_backend_url = $settings["local_backend_url"] ?? "";
$local_backend_key = $settings["local_backend_key"] ?? "dummy";
$local_backend_model =
$settings["local_backend_model"] ?? "claude-local";
$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(
@@ -1741,6 +1917,7 @@ class WP_Agentic_Writer_Settings_V2
return compact(
"api_key",
"brave_search_api_key",
"custom_search_url",
"chat_model",
"clarity_model",
"planning_model",
@@ -1759,13 +1936,20 @@ class WP_Agentic_Writer_Settings_V2
"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",
@@ -1774,6 +1958,7 @@ class WP_Agentic_Writer_Settings_V2
"openrouter_allow_provider_fallbacks",
"memanto_enabled",
"memanto_url",
"memanto_license_key",
"memanto_moorcheh_key",
"settings",
);
@@ -1822,26 +2007,37 @@ class WP_Agentic_Writer_Settings_V2
*/
public function ajax_test_local_backend()
{
check_ajax_referer("wpaw_test_local_backend", "nonce");
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 this URL
$temp_settings = get_option("wp_agentic_writer_settings", []);
// 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()]);
}
@@ -1875,7 +2071,8 @@ class WP_Agentic_Writer_Settings_V2
}
// Temporarily override settings so the client uses the form values.
$temp_settings = get_option("wp_agentic_writer_settings", []);
$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),
@@ -1885,8 +2082,12 @@ class WP_Agentic_Writer_Settings_V2
// Clear health cache so the fresh URL/key are used.
delete_transient("wpaw_memanto_health");
$client = WP_Agentic_Writer_Memanto_Client::get_instance();
$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);