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.
429 lines
13 KiB
PHP
429 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* Refinement REST Controller
|
|
*
|
|
* Handles content refinement operations including block refinement and regeneration.
|
|
*
|
|
* @package WP_Agentic_Writer
|
|
*/
|
|
|
|
/**
|
|
* Class WP_Agentic_Writer_Controller_Refinement
|
|
*
|
|
* REST controller for refinement operations.
|
|
*
|
|
* @since 0.3.0
|
|
*/
|
|
class WP_Agentic_Writer_Controller_Refinement
|
|
{
|
|
/**
|
|
* 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 block refine request.
|
|
*
|
|
* @since 0.1.0
|
|
* @param WP_REST_Request $request REST request.
|
|
* @return WP_REST_Response|WP_Error|void Response or streaming.
|
|
*/
|
|
public function handle_block_refine($request)
|
|
{
|
|
$params = $request->get_json_params();
|
|
$block_id = $params["blockId"] ?? "";
|
|
$block_type = $params["blockType"] ?? "";
|
|
$block_content = $params["blockContent"] ?? "";
|
|
$refinement_request = $params["refinementRequest"] ?? "";
|
|
$article_context = $params["articleContext"] ?? [];
|
|
$post_id = $params["postId"] ?? 0;
|
|
$stream = $params["stream"] ?? false;
|
|
$chat_history = $params["chatHistory"] ?? [];
|
|
|
|
if (empty($block_content) || empty($refinement_request)) {
|
|
return new WP_Error(
|
|
"missing_data",
|
|
__(
|
|
"Block content and refinement request are required.",
|
|
"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 after permission check.
|
|
$post_config = $this->sidebar->resolve_post_config_from_request(
|
|
$params,
|
|
$post_id,
|
|
);
|
|
|
|
// If streaming is requested, use streaming response.
|
|
if ($stream) {
|
|
return $this->sidebar->stream_block_refine(
|
|
$block_id,
|
|
$block_type,
|
|
$block_content,
|
|
$refinement_request,
|
|
$article_context,
|
|
$post_id,
|
|
$post_config,
|
|
);
|
|
}
|
|
|
|
$provider_result = WP_Agentic_Writer_Provider_Manager::get_provider_for_task(
|
|
"refinement",
|
|
);
|
|
$provider = $provider_result->provider;
|
|
|
|
// Build context from article structure.
|
|
$context_str = "\n\nArticle Context:\n";
|
|
$context_str .=
|
|
"Title: " . ($article_context["title"] ?? "Unknown") . "\n";
|
|
|
|
if (!empty($article_context["previousBlock"])) {
|
|
$context_str .=
|
|
"Previous section: " .
|
|
$article_context["previousBlock"]["heading"] .
|
|
"\n";
|
|
}
|
|
|
|
$context_str .= "Current block type: " . $block_type . "\n";
|
|
$context_str .= "Current content:\n" . $block_content . "\n";
|
|
|
|
if (!empty($article_context["nextBlock"])) {
|
|
$context_str .=
|
|
"Next section: " .
|
|
$article_context["nextBlock"]["heading"] .
|
|
"\n";
|
|
}
|
|
|
|
// Add chat history context if available
|
|
$chat_history_context = "";
|
|
if (!empty($chat_history) && is_array($chat_history)) {
|
|
$chat_history_context = "\n\n--- ORIGINAL CONVERSATION ---\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 ---\n";
|
|
$chat_history_context .=
|
|
"This shows the original discussion that led to this article.";
|
|
}
|
|
|
|
// Add plan context if available
|
|
$plan_context = "";
|
|
$plan = get_post_meta($post_id, "_wpaw_plan", true);
|
|
if (!empty($plan) && is_array($plan)) {
|
|
$plan_context = "\n\nOriginal Article Outline:\n";
|
|
if (!empty($plan["title"])) {
|
|
$plan_context .= "Title: {$plan["title"]}\n";
|
|
}
|
|
if (!empty($plan["sections"]) && is_array($plan["sections"])) {
|
|
foreach ($plan["sections"] as $section) {
|
|
$heading = $section["heading"] ?? ($section["title"] ?? "");
|
|
if (!empty($heading)) {
|
|
$plan_context .= "- {$heading}\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$system_prompt = "You are an expert editor helping refine a specific section of an article.
|
|
|
|
{$context_str}
|
|
{$plan_context}
|
|
{$chat_history_context}
|
|
|
|
USER REQUEST: {$refinement_request}
|
|
|
|
TASK:
|
|
Refine the current section content considering:
|
|
1. How it fits into the overall article flow
|
|
2. Consistency with surrounding sections
|
|
3. The original intent from the conversation and outline
|
|
4. The user's specific refinement request
|
|
5. Maintaining the original key information
|
|
|
|
Provide the refined content in Markdown format.
|
|
Keep the same block type (paragraph, heading, list, etc.).";
|
|
|
|
$messages = [
|
|
[
|
|
"role" => "system",
|
|
"content" => $system_prompt,
|
|
],
|
|
[
|
|
"role" => "user",
|
|
"content" => "Please refine this content.",
|
|
],
|
|
];
|
|
|
|
$response = $provider->chat(
|
|
$messages,
|
|
["temperature" => 0.7],
|
|
"execution",
|
|
);
|
|
|
|
if (is_wp_error($response)) {
|
|
return new WP_Error(
|
|
"refinement_error",
|
|
$response->get_error_message(),
|
|
["status" => 500],
|
|
);
|
|
}
|
|
|
|
// Parse refined content as Gutenberg blocks.
|
|
$blocks = WP_Agentic_Writer_Markdown_Parser::parse(
|
|
$response["content"],
|
|
);
|
|
|
|
// MEMANTO: Remember block refinement.
|
|
do_action(
|
|
"wpaw_memanto_block_refined",
|
|
$post_id,
|
|
$block_id,
|
|
$refinement_request,
|
|
);
|
|
|
|
// Track cost (always track for debugging).
|
|
$this->sidebar->track_ai_cost(
|
|
$post_id,
|
|
$response["model"] ?? "",
|
|
"block_refinement",
|
|
$response["input_tokens"] ?? 0,
|
|
$response["output_tokens"] ?? 0,
|
|
$response["cost"] ?? 0,
|
|
$provider_result,
|
|
"",
|
|
"success",
|
|
);
|
|
|
|
return new WP_REST_Response(
|
|
[
|
|
"blocks" => $blocks,
|
|
"blockId" => $block_id,
|
|
"cost" => $response["cost"] ?? 0,
|
|
"provider_metadata" => $this->sidebar->build_provider_metadata(
|
|
$provider_result,
|
|
$response["model"] ?? "",
|
|
),
|
|
],
|
|
200,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle regenerate block request.
|
|
*
|
|
* @since 0.1.0
|
|
* @param WP_REST_Request $request REST request.
|
|
* @return WP_REST_Response|WP_Error Response.
|
|
*/
|
|
public function handle_regenerate_block($request)
|
|
{
|
|
// Check rate limit.
|
|
$rate_limit = WPAW_Rate_Limiter::check("refine");
|
|
if (is_wp_error($rate_limit)) {
|
|
return $rate_limit;
|
|
}
|
|
|
|
$params = $request->get_json_params();
|
|
$block_content = $params["blockContent"] ?? "";
|
|
$context = $params["context"] ?? "";
|
|
$post_id = $params["postId"] ?? 0;
|
|
|
|
if (empty($block_content)) {
|
|
return new WP_Error(
|
|
"no_content",
|
|
__("Block content 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],
|
|
);
|
|
}
|
|
|
|
// Get provider for writing task.
|
|
$provider_result = WP_Agentic_Writer_Provider_Manager::get_provider_for_task(
|
|
"writing",
|
|
);
|
|
$provider = $provider_result->provider;
|
|
|
|
$messages = [
|
|
[
|
|
"role" => "system",
|
|
"content" =>
|
|
"You are an expert technical writer. Rewrite the provided content to improve it while maintaining the same meaning and key information.",
|
|
],
|
|
[
|
|
"role" => "user",
|
|
"content" => "Context: {$context}\n\nOriginal content:\n\n{$block_content}\n\nPlease rewrite this content.",
|
|
],
|
|
];
|
|
|
|
$response = $provider->chat(
|
|
$messages,
|
|
["temperature" => 0.8],
|
|
"execution",
|
|
);
|
|
|
|
if (is_wp_error($response)) {
|
|
// Track failed attempt for observability.
|
|
$this->sidebar->track_ai_cost(
|
|
$post_id,
|
|
WPAW_Model_Registry::get_default_model("writing"),
|
|
"regeneration",
|
|
0,
|
|
0,
|
|
0,
|
|
$provider_result,
|
|
"",
|
|
"error",
|
|
);
|
|
return new WP_Error(
|
|
"regeneration_error",
|
|
$response->get_error_message(),
|
|
["status" => 500],
|
|
);
|
|
}
|
|
|
|
// Track cost (always track for debugging).
|
|
$this->sidebar->track_ai_cost(
|
|
$post_id,
|
|
$response["model"] ?? "",
|
|
"regeneration",
|
|
$response["input_tokens"] ?? 0,
|
|
$response["output_tokens"] ?? 0,
|
|
$response["cost"] ?? 0,
|
|
$provider_result,
|
|
"",
|
|
"success",
|
|
);
|
|
|
|
return new WP_REST_Response(
|
|
[
|
|
"content" => $response["content"],
|
|
"cost" => $response["cost"] ?? 0,
|
|
"provider_metadata" => $this->sidebar->build_provider_metadata(
|
|
$provider_result,
|
|
$response["model"] ?? "",
|
|
),
|
|
],
|
|
200,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle refine from chat request.
|
|
*
|
|
* @since 0.1.0
|
|
* @param WP_REST_Request $request REST request.
|
|
* @return void Streams response to client.
|
|
*/
|
|
public function handle_refine_from_chat($request)
|
|
{
|
|
$params = $request->get_json_params();
|
|
$message = $params["topic"] ?? "";
|
|
$selected_block = $params["selectedBlockClientId"] ?? "";
|
|
$post_id = $params["postId"] ?? 0;
|
|
$session_id = $this->sidebar->resolve_or_create_session_id(
|
|
$params["sessionId"] ?? "",
|
|
$post_id,
|
|
);
|
|
$blocks_to_refine = $params["blocksToRefine"] ?? [];
|
|
$all_blocks = $params["allBlocks"] ?? [];
|
|
$diff_plan = !empty($params["diffPlan"]);
|
|
$selective_refine = !empty($params["selectiveRefine"]);
|
|
$audit_context =
|
|
isset($params["auditContext"]) && is_array($params["auditContext"])
|
|
? $params["auditContext"]
|
|
: [];
|
|
|
|
if (empty($blocks_to_refine) || !is_array($blocks_to_refine)) {
|
|
return new WP_Error(
|
|
"no_blocks_mentioned",
|
|
__(
|
|
"No valid blocks found to refine. Try mentioning blocks like @this, @previous, or specific blocks like @paragraph-1",
|
|
"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 after permission check.
|
|
$post_config = $this->sidebar->resolve_post_config_from_request(
|
|
$params,
|
|
$post_id,
|
|
);
|
|
|
|
// Stream refinement for each mentioned block
|
|
$this->sidebar->stream_refinement_from_chat(
|
|
$blocks_to_refine,
|
|
$message,
|
|
$selected_block,
|
|
$post_id,
|
|
$all_blocks,
|
|
$diff_plan,
|
|
$post_config,
|
|
$session_id,
|
|
$selective_refine,
|
|
$audit_context,
|
|
);
|
|
|
|
// Return early to avoid REST API trying to send headers after streaming
|
|
exit();
|
|
}
|
|
}
|