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,616 @@
<?php
/**
* Writing REST Controller
*
* Handles article writing and formatting operations.
*
* @package WP_Agentic_Writer
*/
/**
* Class WP_Agentic_Writer_Controller_Writing
*
* REST controller for writing operations including article execution and block formatting.
*
* @since 0.3.0
*/
class WP_Agentic_Writer_Controller_Writing
{
/**
* 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 execute article request.
*
* @since 0.1.0
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error Response.
*/
public function handle_execute_article($request)
{
// Check rate limit.
$rate_limit = WPAW_Rate_Limiter::check("execute_article");
if (is_wp_error($rate_limit)) {
return $rate_limit;
}
$params = $request->get_json_params();
$post_id = $params["postId"] ?? 0;
$session_id = $this->sidebar->resolve_or_create_session_id(
$params["sessionId"] ?? "",
$post_id,
);
$stream = $params["stream"] ?? false;
$recommended_title = "";
$chat_history = $params["chatHistory"] ?? [];
$post_config = $this->sidebar->resolve_post_config_from_request(
$params,
$post_id,
);
$post_config_context = $this->sidebar->build_post_config_context($post_config);
$stored_language = get_post_meta(
$post_id,
"_wpaw_detected_language",
true,
);
$detected_language = $params["detectedLanguage"] ?? $stored_language;
$effective_language = $this->sidebar->resolve_language_preference(
$post_config,
$detected_language,
);
// Auto-save post and link conversation if needed (only for post_id = 0)
if (empty($post_id) && !empty($session_id)) {
$post_id = $this->sidebar->ensure_conversation_linked_to_post(
$session_id,
$post_id,
);
}
// 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],
);
}
// Get plan from post meta.
$plan = get_post_meta($post_id, "_wpaw_plan", true);
if (empty($plan)) {
return new WP_Error(
"no_plan",
__(
"No plan found. Please generate a plan first.",
"wp-agentic-writer",
),
["status" => 400],
);
}
if ($stream) {
// For streaming, link conversation to post BEFORE getting plan from meta
if (empty($post_id) && !empty($session_id)) {
$post_id = $this->sidebar->ensure_conversation_linked_to_post(
$session_id,
$post_id,
);
}
// Now get plan after potentially having a valid post_id
$plan = get_post_meta($post_id, "_wpaw_plan", true);
if (empty($plan)) {
echo "data: " .
wp_json_encode([
"type" => "error",
"message" =>
"No plan found. Please generate a plan first.",
]) .
"\n\n";
flush();
return;
}
$this->sidebar->stream_execute_article(
$plan,
$post_id,
$post_config,
$effective_language,
$session_id,
);
exit();
}
$plan = $this->sidebar->ensure_plan_sections_with_tasks($plan);
// MEMANTO: Plan execution implies approval.
do_action("wpaw_memanto_plan_approved", $post_id, $plan);
// Update post title from the plan title when available.
if (!empty($plan["title"])) {
$recommended_title = sanitize_text_field($plan["title"]);
if ($post_id > 0) {
$post = get_post($post_id);
if ($post && current_user_can("edit_post", $post_id)) {
if (empty($post->post_title)) {
wp_update_post([
"ID" => $post_id,
"post_title" => $recommended_title,
]);
}
}
}
}
// Get provider for writing task.
$provider_result = WP_Agentic_Writer_Provider_Manager::get_provider_for_task(
"writing",
);
$provider = $provider_result->provider;
$image_instruction = "IMAGE SUGGESTIONS:
- Suggest where images would enhance understanding
- Place image suggestions on their own line using this format: [IMAGE: descriptive alt text]
- Be strategic: only suggest images where they add real value (diagrams, screenshots, visual examples)
- Good places for images: after introductions, before complex explanations, to show examples
- Maximum 1-2 image suggestions per section
- Example: [IMAGE: Screenshot of the plugin settings panel showing the API key field]";
if (empty($post_config["include_images"])) {
$image_instruction = "IMAGE SUGGESTIONS:
- Do NOT include any image suggestions or [IMAGE: ...] placeholders.";
}
$language_instruction = $this->sidebar->build_language_instruction(
$effective_language,
"article content",
);
// Build chat history context for continuity
$chat_history_context = "";
if (!empty($chat_history) && is_array($chat_history)) {
$chat_history_context = "\n\n--- CONVERSATION CONTEXT ---\n";
foreach ($chat_history as $msg) {
$role = isset($msg["role"]) ? ucfirst($msg["role"]) : "Unknown";
$content = isset($msg["content"]) ? $msg["content"] : "";
if (
!empty($content) &&
"system" !== strtolower($msg["role"] ?? "")
) {
$chat_history_context .= "{$role}: {$content}\n\n";
}
}
$chat_history_context .= "--- END CONVERSATION CONTEXT ---\n";
$chat_history_context .=
"Use the above conversation to understand the user's intent and preferences for this article.";
}
// Build SEO instructions if SEO is enabled
$seo_instruction = "";
$internal_links_instruction = "";
if (
!empty($post_config["seo_enabled"]) &&
!empty($post_config["seo_focus_keyword"])
) {
$focus_keyword = $post_config["seo_focus_keyword"];
$seo_instruction = "\n\nSEO OPTIMIZATION REQUIREMENTS (CRITICAL - MUST FOLLOW):
- Focus Keyword: \"{$focus_keyword}\"
- MANDATORY: Include the exact focus keyword \"{$focus_keyword}\" in the article title (preferably at the beginning)
- MANDATORY: Use the focus keyword in the FIRST paragraph (within the first 100 words)
- Use the focus keyword 5-8 times naturally throughout the article
- Include the focus keyword in:
* At least 2 H2 or H3 subheadings
* The conclusion paragraph
- Include 2-3 authoritative outbound links to reputable sources (Wikipedia, official documentation, industry leaders)
- When suggesting images, include the focus keyword or related terms in the alt text
- Keep the article title under 60 characters";
// Get internal link suggestions
$internal_links = $this->sidebar->suggest_internal_links(
$post_id,
$focus_keyword,
3,
);
if (!empty($internal_links)) {
$internal_links_instruction =
"\n\nINTERNAL LINKS (optional - use where contextually relevant):\n";
foreach ($internal_links as $link) {
$internal_links_instruction .= "- [{$link["title"]}]({$link["url"]})\n";
}
$internal_links_instruction .=
"Naturally incorporate 1-2 of these internal links where they add value to the reader. Use descriptive anchor text, not 'click here'.";
}
}
// Build system prompt for article generation.
$system_prompt = "You are an industry practitioner sharing insights with a colleague. Write engaging, high-information-density content based on the provided article plan.
ANTI-ROBOT RULES:
- BANNED WORDS: delve, furthermore, moreover, crucial, paramount, landscape, testament, in today's digital world, in conclusion.
- BANNED PATTERNS: Do not use transition words to start paragraphs. Do not summarize what you are about to say. Do not summarize what you just said.
- BURSTINESS: Mix very short, punchy sentences (3-5 words) with longer, descriptive ones. Avoid uniform sentence length.
- TONE: Conversational, direct, pragmatic. Do not sound like an academic 'expert' or textbook.
GEO/SEO STRATEGY:
- Answer the implicit user intent directly and immediately in the first paragraph.
- Maximize information density: high ratio of facts/insights to total word count. Remove filler adjectives.
- Use bullet points or numbered lists where they make data easier to scan.
CRITICAL LANGUAGE REQUIREMENT:
{$language_instruction}
{$post_config_context}
{$chat_history_context}
Follow these guidelines:
- Use the tone specified in POST CONFIG if provided; otherwise be conversational but professional
- Embed secondary keywords naturally as concepts, without forcing exact matches
- For code blocks, use proper syntax highlighting (e.g., ```php)
- Code typography: Use plain ASCII quotes inside code. Do NOT use smart quotes.
- Write for the specified difficulty level
{$seo_instruction}
{$internal_links_instruction}
IMAGE SUGGESTIONS:
- Suggest where images would enhance understanding (diagrams, screenshots)
- Place image suggestions on their own line: [IMAGE: descriptive alt text]
- Maximum 1 image per section
{$image_instruction}";
// Generate content for each section.
$blocks = [];
$total_cost = 0;
$sections_to_write = [];
foreach ($plan["sections"] as $index => $section) {
$status = $section["status"] ?? "pending";
if ("done" === $status) {
continue;
}
$sections_to_write[$index] = $section;
}
foreach ($sections_to_write as $section) {
$heading = $section["heading"] ?? ($section["title"] ?? "");
$section_prompt = $heading
? "Write the \"{$heading}\" section.\n\n"
: "Write the next section.\n\n";
$section_prompt .= "Content requirements:\n";
if (!empty($section["content"]) && is_array($section["content"])) {
foreach ($section["content"] as $item) {
if (!empty($item["content"])) {
$section_prompt .= "- {$item["content"]}\n";
}
}
}
$messages = [
[
"role" => "system",
"content" => $system_prompt,
],
[
"role" => "user",
"content" => $section_prompt,
],
];
$response = $provider->chat(
$messages,
["temperature" => 0.8],
"execution",
);
if (is_wp_error($response)) {
return new WP_Error(
"execution_error",
$response->get_error_message(),
["status" => 500],
);
}
// Add section blocks.
if ($heading) {
$blocks[] = [
"type" => "heading",
"content" => $heading,
"level" => 2,
];
}
$section_blocks = WP_Agentic_Writer_Markdown_Parser::parse(
$response["content"],
);
if (!empty($section_blocks)) {
$first_block = $section_blocks[0];
if (
isset($first_block["blockName"]) &&
"core/heading" === $first_block["blockName"]
) {
$first_heading = $first_block["attrs"]["content"] ?? "";
if (
$heading &&
$first_heading &&
0 === strcasecmp(trim($first_heading), trim($heading))
) {
array_shift($section_blocks);
}
}
foreach ($section_blocks as $block) {
$blocks[] = $block;
}
} else {
$blocks[] = [
"type" => "paragraph",
"content" => $response["content"],
];
}
$total_cost += $response["cost"];
// MEMANTO: Remember section written.
$section_id = $section["id"] ?? sanitize_title($heading);
do_action(
"wpaw_memanto_section_written",
$post_id,
$section_id,
$heading,
);
}
if (!empty($sections_to_write)) {
foreach (array_keys($sections_to_write) as $section_index) {
$plan["sections"][$section_index]["status"] = "done";
}
if ($post_id > 0) {
update_post_meta($post_id, "_wpaw_plan", $plan);
}
}
// Track total cost.
$this->sidebar->track_ai_cost(
$post_id,
$this->sidebar->get_provider_execution_model($provider, "execution"),
"execution",
0,
0,
$total_cost,
$provider_result,
"",
"success",
);
return new WP_REST_Response(
[
"blocks" => $blocks,
"cost" => $total_cost,
"recommended_title" => $recommended_title,
"provider_metadata" => $this->sidebar->build_provider_metadata(
$provider_result,
$this->sidebar->get_provider_execution_model($provider, "execution"),
),
],
200,
);
}
/**
* Handle reformat blocks request.
*
* @since 0.1.0
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error Response.
*/
public function handle_reformat_blocks($request)
{
$params = $request->get_json_params();
$blocks = $params["blocks"] ?? [];
$post_id = $params["postId"] ?? 0;
$recommended_title = "";
$title_updated = false;
if (empty($blocks) || !is_array($blocks)) {
return new WP_Error(
"no_blocks",
__("Blocks are required to reformat.", "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],
);
}
$results = [];
if ($post_id > 0) {
$plan = get_post_meta($post_id, "_wpaw_plan", true);
if (is_array($plan) && !empty($plan["title"])) {
$recommended_title = sanitize_text_field($plan["title"]);
}
}
foreach ($blocks as $block) {
$client_id =
$block["clientId"] ?? ($block["attrs"]["clientId"] ?? "");
$block_type =
$block["name"] ?? ($block["blockName"] ?? "core/paragraph");
$block_attrs = $block["attributes"] ?? ($block["attrs"] ?? []);
if (empty($client_id)) {
continue;
}
if ("core/paragraph" !== $block_type) {
continue;
}
$content = $this->sidebar->extract_block_content_from_attrs(
$block_type,
$block_attrs,
);
if ("" === trim((string) $content)) {
continue;
}
$parsed_blocks = WP_Agentic_Writer_Markdown_Parser::parse($content);
if (empty($parsed_blocks)) {
continue;
}
$results[] = [
"clientId" => $client_id,
"blocks" => $parsed_blocks,
];
}
if ($post_id > 0 && "" !== $recommended_title) {
$post = get_post($post_id);
if ($post && current_user_can("edit_post", $post_id)) {
if (empty($post->post_title)) {
wp_update_post([
"ID" => $post_id,
"post_title" => $recommended_title,
]);
$title_updated = true;
}
}
}
return new WP_REST_Response(
[
"results" => $results,
"recommended_title" => $recommended_title,
"title_updated" => $title_updated,
],
200,
);
}
/**
* Handle save section blocks request.
*
* @since 0.1.0
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error Response.
*/
public function handle_save_section_blocks($request)
{
$params = $request->get_json_params();
$post_id = intval($params["postId"] ?? 0);
$section_id = sanitize_text_field($params["sectionId"] ?? "");
$block_ids = $params["blockIds"] ?? [];
if ($post_id <= 0 || empty($section_id) || !is_array($block_ids)) {
return new WP_Error(
"invalid_section_blocks",
__(
"Invalid section block mapping request.",
"wp-agentic-writer",
),
["status" => 400],
);
}
if (!$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],
);
}
$block_ids = array_values(
array_filter(array_map("sanitize_text_field", $block_ids)),
);
$mapping = get_post_meta($post_id, "_wpaw_section_blocks", true);
if (!is_array($mapping)) {
$mapping = [];
}
$mapping[$section_id] = $block_ids;
update_post_meta($post_id, "_wpaw_section_blocks", $mapping);
return new WP_REST_Response(
[
"success" => true,
"sectionId" => $section_id,
"blockCount" => count($block_ids),
],
200,
);
}
/**
* Handle get section blocks request.
*
* @since 0.1.0
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error Response.
*/
public function handle_get_section_blocks($request)
{
$post_id = intval($request["post_id"] ?? 0);
if ($post_id <= 0) {
return new WP_Error(
"invalid_post",
__("Invalid post ID.", "wp-agentic-writer"),
["status" => 400],
);
}
if (!$this->sidebar->check_post_permission($post_id)) {
return new WP_Error(
"forbidden",
__(
"You do not have permission to access this post.",
"wp-agentic-writer",
),
["status" => 403],
);
}
$mapping = get_post_meta($post_id, "_wpaw_section_blocks", true);
if (!is_array($mapping)) {
$mapping = [];
}
return new WP_REST_Response(
[
"sectionBlocks" => $mapping,
],
200,
);
}
}