refactor: Cleanup git state - commit all staged changes

Major refactoring cleanup:
- Add new controller architecture (class-controller-*.php)
- Add new settings-v2 UI (views/settings-v2/)
- Add new CSS architecture (agentic-sidebar.css, tokens)
- Add esbuild build pipeline (scripts/build.js, package.json)
- Add composer dependencies (vendor/)
- Add frontend src directory (assets/js/src/index.jsx)
- Add documentation files
- Remove old/obsolete files (class-settings.php, old CSS)

This commits all pending changes from previous refactoring efforts.
This commit is contained in:
Dwindi Ramadhana
2026-06-17 05:27:58 +07:00
parent d3f142222c
commit 690991c526
7963 changed files with 941566 additions and 67372 deletions

View File

@@ -0,0 +1,589 @@
<?php
/**
* Planning REST Controller
*
* Handles article planning operations including outline generation and revision.
*
* @package WP_Agentic_Writer
*/
/**
* Class WP_Agentic_Writer_Controller_Planning
*
* REST controller for planning operations.
*
* @since 0.3.0
*/
class WP_Agentic_Writer_Controller_Planning
{
/**
* Sidebar instance for dependency access.
*
* @var WP_Agentic_Writer_Gutenberg_Sidebar
*/
private $sidebar;
/**
* Constructor.
*
* @since 0.3.0
* @param WP_Agentic_Writer_Gutenberg_Sidebar $sidebar Sidebar instance.
*/
public function __construct($sidebar)
{
$this->sidebar = $sidebar;
}
/**
* Handle generate plan request.
*
* @since 0.1.0
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error Response.
*/
public function handle_generate_plan($request)
{
// Check rate limit.
$rate_limit = WPAW_Rate_Limiter::check("generate_plan");
if (is_wp_error($rate_limit)) {
return $rate_limit;
}
$params = $request->get_json_params();
$topic = $params["topic"] ?? "";
$context = $params["context"] ?? "";
$post_id = $params["postId"] ?? 0;
$session_id = $this->sidebar->resolve_or_create_session_id(
$params["sessionId"] ?? "",
$post_id,
);
$auto_execute = $params["autoExecute"] ?? false;
$stream = $params["stream"] ?? false;
$chat_history = $params["chatHistory"] ?? [];
$post_config = $this->sidebar->resolve_post_config_from_request(
$params,
$post_id,
);
$article_length =
$post_config["article_length"] ??
($params["articleLength"] ?? "medium");
$clarification_answers = $params["clarificationAnswers"] ?? [];
$detected_language = $params["detectedLanguage"] ?? "auto";
$clarified_language = $this->sidebar->resolve_language_from_clarification_answers(
$clarification_answers,
);
$effective_language = $this->sidebar->resolve_language_preference(
$post_config,
$clarified_language ?: $detected_language,
);
$post_config_context = $this->sidebar->build_post_config_context(
$post_config,
);
$web_search_options = $this->sidebar->get_web_search_options(
$post_config,
);
if (empty($topic)) {
return new WP_Error(
"no_topic",
__("Topic is required.", "wp-agentic-writer"),
["status" => 400],
);
}
// Check post permission if post_id is provided.
if ($post_id > 0 && !$this->sidebar->check_post_permission($post_id)) {
return new WP_Error(
"forbidden",
__(
"You do not have permission to edit this post.",
"wp-agentic-writer",
),
["status" => 403],
);
}
$context_builder = WP_Agentic_Writer_Context_Builder::get_instance();
$context_package = $context_builder->build_for_task(
"planning",
$session_id,
$post_id,
array_merge($params, [
"chatHistory" => $chat_history,
"postConfig" => $post_config,
]),
);
// If streaming is requested, delegate to sidebar streaming method.
if ($stream) {
return $this->sidebar->stream_generate_plan(
$topic,
$context,
$post_id,
$auto_execute,
$article_length,
$clarification_answers,
$effective_language,
$post_config,
$chat_history,
$session_id,
);
}
// Get provider for planning task.
$provider_result = WP_Agentic_Writer_Provider_Manager::get_provider_for_task(
"planning",
);
$provider = $provider_result->provider;
// Build prompt for plan generation.
$plan_language_instruction = $this->sidebar->build_language_instruction(
$effective_language,
"article plan (title, section headings, descriptions)",
);
$system_prompt = "You are an Information Architect and SEO/GEO Strategist. Your task is to outline a high-information-density article based on the user's topic and context.
ANTI-ROBOT RULES:
- Never use generic intros or 'throat-clearing' fluff.
- Avoid academic, pompous, or 'expert' posturing.
- Headings must provide direct value or ask specific questions the article will answer.
GEO/SEO STRATEGY:
- Design the outline for Generative Engine Optimization (GEO): sections must flow logically to answer the user's core intent comprehensively.
- Suggest strategic use of tables, bullet points, and Q&A formats where they maximize information density.
- Incorporate secondary entities and related concepts naturally to show topical depth.
CRITICAL LANGUAGE REQUIREMENT:
{$plan_language_instruction}
Keep JSON keys in English for parsing, but write every user-visible JSON value (title, headings, descriptions, labels) in the required article language.
{$post_config_context}
Generate a JSON outline with the following structure:
{
\"title\": \"Article title\",
\"meta\": {
\"reading_time\": \"5 min\",
\"difficulty\": \"intermediate\",
\"cost_estimate\": 0.70
},
\"sections\": [
{
\"id\": \"unique-section-id\",
\"status\": \"pending\",
\"type\": \"section\",
\"heading\": \"Section heading\",
\"content\": [
{
\"type\": \"paragraph\",
\"content\": \"Brief description of what this section should cover\"
}
]
}
]
}
Return only valid raw JSON that matches this schema. Do not wrap it in markdown fences and do not add explanatory text.
Keep sections focused and actionable. Include H2 headings only. For technical articles, suggest code blocks.";
$messages = [
[
"role" => "system",
"content" => $system_prompt,
],
[
"role" => "user",
"content" => "Topic: {$topic}\n\nContext:\n{$context_package["working_context"]}\n\n{$context_package["research_context"]}",
],
];
// Generate plan.
$this->sidebar->maybe_inject_brave_search(
$messages,
$provider,
$web_search_options,
);
$response = $provider->chat(
$messages,
array_merge(
[
"temperature" => 0.7,
"max_tokens" => 2200,
],
$web_search_options,
),
"planning",
);
// Debug: log the provider type and response
$provider_class = get_class($provider);
wpaw_debug_log("Plan generation using provider: " . $provider_class);
if (is_wp_error($response)) {
wpaw_debug_log(
"Plan generation error: " . $response->get_error_message(),
);
return new WP_Error(
"plan_generation_error",
$response->get_error_message(),
["status" => 500],
);
}
// Extract JSON from response.
$content = $response["content"] ?? "";
$plan_json = WP_Agentic_Writer_Sidebar_Helpers::extract_plan_from_response(
$content,
$topic,
);
// Debug: log the raw response
wpaw_debug_log(
"Plan generation raw response length: " . strlen($content),
);
if (empty(trim((string) $content))) {
$model_used = $response["model"] ?? "unknown";
return new WP_Error(
"empty_response",
sprintf(
__(
"The AI model (%s) returned an empty response. Try a different planning model or simplify your topic.",
"wp-agentic-writer",
),
$model_used,
),
["status" => 500],
);
}
if (null === $plan_json) {
wpaw_debug_log(
"extract_plan_from_response returned null. Content preview: " .
substr($content, 0, 500),
);
return new WP_Error(
"invalid_json",
sprintf(
/* translators: %s: model output preview */
__(
'The AI responded but the outline couldn\'t be parsed as JSON. Try again — this is usually a one-time formatting issue. Preview: %s',
"wp-agentic-writer",
),
$this->sidebar->build_model_output_preview($content),
),
["status" => 500],
);
}
$plan_json = $this->sidebar->ensure_plan_sections_with_tasks($plan_json);
// MEMANTO: Remember plan was generated.
do_action("wpaw_memanto_plan_generated", $post_id, $plan_json);
// Persist planning exchange into session history.
if (!empty($session_id)) {
$context_service = WP_Agentic_Writer_Context_Service::get_instance();
$context_service->update_session_context($session_id, [
"working_summary" => [
"text" => WP_Agentic_Writer_Sidebar_Helpers::build_memory_summary_from_plan(
$plan_json,
),
"updated_at" => current_time("c"),
"source_message_count" => 0,
],
]);
$context_service->add_message($session_id, [
"role" => "user",
"content" => trim((string) $topic),
"timestamp" => current_time("c"),
]);
$context_service->add_message($session_id, [
"role" => "assistant",
"type" => "plan",
"plan" => $plan_json,
"content" => $this->sidebar->build_plan_summary_for_session(
$plan_json,
$post_config,
),
"timestamp" => current_time("c"),
]);
}
// Store plan in post meta.
if ($post_id > 0) {
update_post_meta($post_id, "_wpaw_plan", $plan_json);
update_post_meta(
$post_id,
"_wpaw_detected_language",
$effective_language,
);
$summary = WP_Agentic_Writer_Sidebar_Helpers::build_memory_summary_from_plan(
$plan_json,
);
$this->sidebar->update_post_memory($post_id, [
"summary" => $summary,
"last_prompt" => $topic,
"last_intent" => "generate",
]);
}
// Track cost with provider metadata.
$this->sidebar->track_ai_cost(
$post_id,
$response["model"] ?? "",
"planning",
$response["input_tokens"] ?? 0,
$response["output_tokens"] ?? 0,
$response["cost"] ?? 0,
$provider_result,
$session_id,
"success",
);
return new WP_REST_Response(
[
"plan" => $plan_json,
"cost" => $response["cost"] ?? 0,
"web_search_results" => $response["web_search_results"] ?? [],
"context_audit" => $context_package["audit"] ?? [],
"provider_metadata" => $this->sidebar->build_provider_metadata(
$provider_result,
$response["model"] ?? "",
),
],
200,
);
}
/**
* Handle revise plan request.
*
* @since 0.1.0
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error Response.
*/
public function handle_revise_plan($request)
{
$params = $request->get_json_params();
$instruction = $params["instruction"] ?? "";
$plan = $params["plan"] ?? [];
$post_id = $params["postId"] ?? 0;
$session_id = $this->sidebar->resolve_or_create_session_id(
$params["sessionId"] ?? "",
$post_id,
);
if (empty($instruction)) {
return new WP_Error(
"no_instruction",
__("Instruction is required.", "wp-agentic-writer"),
["status" => 400],
);
}
if (empty($plan) || !is_array($plan)) {
return new WP_Error(
"no_plan",
__("Plan is required to revise.", "wp-agentic-writer"),
["status" => 400],
);
}
// Check post permission BEFORE reading post data.
if ($post_id > 0 && !$this->sidebar->check_post_permission($post_id)) {
return new WP_Error(
"forbidden",
__(
"You do not have permission to edit this post.",
"wp-agentic-writer",
),
["status" => 403],
);
}
// Only read post config/meta after permission check.
$post_config = $this->sidebar->resolve_post_config_from_request(
$params,
$post_id,
);
$post_config_context = $this->sidebar->build_post_config_context(
$post_config,
);
$effective_language = $this->sidebar->resolve_language_preference(
$post_config,
get_post_meta($post_id, "_wpaw_detected_language", true),
);
$plan_language_instruction = $this->sidebar->build_language_instruction(
$effective_language,
"article plan (title, section headings, descriptions)",
);
$web_search_options = $this->sidebar->get_web_search_options(
$post_config,
);
$provider_result = WP_Agentic_Writer_Provider_Manager::get_provider_for_task(
"planning",
);
$provider = $provider_result->provider;
$memory_context = WP_Agentic_Writer_Sidebar_Helpers::get_post_memory_context(
$post_id,
);
$context_builder = WP_Agentic_Writer_Context_Builder::get_instance();
$context_package = $context_builder->build_for_task(
"plan_revision",
$session_id,
$post_id,
array_merge($params, [
"plan" => $plan,
"postConfig" => $post_config,
]),
);
$system_prompt = "You are an expert content strategist. Revise the provided outline based on the user's instruction.
{$plan_language_instruction}
{$post_config_context}
{$memory_context}
IMPORTANT:
- Return ONLY valid raw JSON matching the plan schema below.
- Do not wrap in markdown code fences or add explanatory text.
- Keep the same structure: title, meta, sections with id/status/type/heading/content.
Schema:
{
\"title\": \"Article title\",
\"meta\": { \"reading_time\": \"5 min\", \"difficulty\": \"intermediate\", \"cost_estimate\": 0.70 },
\"sections\": [
{
\"id\": \"unique-section-id\",
\"status\": \"pending\",
\"type\": \"section\",
\"heading\": \"Section heading\",
\"content\": [{\"type\": \"paragraph\", \"content\": \"Description\"]}
}
]
}";
$messages = [
[
"role" => "system",
"content" => $system_prompt,
],
[
"role" => "user",
"content" => "Revision instruction: {$instruction}\n\nExisting outline:\n" .
wp_json_encode($plan) .
"\n\nContext:\n{$context_package["working_context"]}",
],
];
// Generate revised plan.
$this->sidebar->maybe_inject_brave_search(
$messages,
$provider,
$web_search_options,
);
$response = $provider->chat(
$messages,
array_merge(["temperature" => 0.6], $web_search_options),
"planning",
);
if (is_wp_error($response)) {
return new WP_Error(
"plan_revision_error",
$response->get_error_message(),
["status" => 500],
);
}
$plan_json = WP_Agentic_Writer_Sidebar_Helpers::extract_plan_from_response(
$response["content"],
$instruction,
$plan,
);
if (null === $plan_json) {
return new WP_Error(
"plan_revision_invalid",
__("Failed to generate valid plan JSON.", "wp-agentic-writer"),
["status" => 500],
);
}
$plan_json = $this->sidebar->ensure_plan_sections_with_tasks(
$plan_json,
$plan,
);
// MEMANTO: Remember plan revision (implies previous plan was rejected).
do_action("wpaw_memanto_plan_rejected", $post_id, $instruction);
if ($post_id > 0) {
update_post_meta($post_id, "_wpaw_plan", $plan_json);
if (!empty($effective_language)) {
update_post_meta(
$post_id,
"_wpaw_detected_language",
$effective_language,
);
}
$summary = WP_Agentic_Writer_Sidebar_Helpers::build_memory_summary_from_plan(
$plan_json,
);
$this->sidebar->update_post_memory($post_id, [
"summary" => $summary,
"last_prompt" => $instruction,
"last_intent" => "plan",
]);
}
if (!empty($session_id)) {
$context_service = WP_Agentic_Writer_Context_Service::get_instance();
$context_service->append_session_context_item(
$session_id,
"plan_versions",
[
"instruction" => sanitize_text_field($instruction),
"plan" => $plan,
],
10,
);
$context_service->update_session_context($session_id, [
"working_summary" => [
"text" => WP_Agentic_Writer_Sidebar_Helpers::build_memory_summary_from_plan(
$plan_json,
),
"updated_at" => current_time("c"),
"source_message_count" => 0,
],
]);
}
// Track cost with provider metadata.
$this->sidebar->track_ai_cost(
$post_id,
$response["model"] ?? "",
"planning",
$response["input_tokens"] ?? 0,
$response["output_tokens"] ?? 0,
$response["cost"] ?? 0,
$provider_result,
$session_id,
"success",
);
return new WP_REST_Response(
[
"plan" => $plan_json,
"cost" => $response["cost"] ?? 0,
"context_audit" => $context_package["audit"] ?? [],
"provider_metadata" => $this->sidebar->build_provider_metadata(
$provider_result,
$response["model"] ?? "",
),
],
200,
);
}
}