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:
589
includes/class-controller-planning.php
Normal file
589
includes/class-controller-planning.php
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user