Files
wp-agentic-writer/includes/class-controller-conversation.php
Dwindi Ramadhana 690991c526 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.
2026-06-17 05:27:58 +07:00

503 lines
16 KiB
PHP

<?php
/**
* Conversation REST Controller
*
* Handles conversation CRUD operations.
*
* @package WP_Agentic_Writer
*/
/**
* Class WP_Agentic_Writer_Controller_Conversation
*
* REST controller for conversation operations.
*
* @since 0.3.0
*/
class WP_Agentic_Writer_Controller_Conversation {
/**
* 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 get conversations request.
*
* Lists all conversations for the current user, optionally filtered by post.
*
* @since 0.1.4
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error
*/
public function handle_get_conversations( $request ) {
$manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
$status = sanitize_text_field( $request->get_param( 'status' ) ?: 'active' );
$limit = (int) $request->get_param( 'limit' ) ?: 20;
$post_id = (int) $request->get_param( 'post_id' ) ?: 0;
// If post_id is specified, check authorization before returning session.
if ( $post_id > 0 ) {
// Authorization: User must be able to edit this post.
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return new WP_Error(
'forbidden',
__(
'You do not have permission to access this post.',
'wp-agentic-writer',
),
[ 'status' => 403 ],
);
}
$sessions = $manager->get_sessions_for_post( $post_id );
return new WP_REST_Response(
[
'sessions' => $sessions,
'count' => count( $sessions ),
],
200,
);
}
if ( $request->get_param( 'uncompleted' ) ) {
$sessions = $manager->get_uncompleted_sessions( $limit );
} else {
$sessions = $manager->get_user_sessions( $status, $limit );
}
return new WP_REST_Response(
[
'sessions' => $sessions,
'count' => count( $sessions ),
],
200,
);
}
/**
* Handle create conversation request.
*
* @since 0.1.4
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error
*/
public function handle_create_conversation( $request ) {
$params = $request->get_json_params();
$manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
$post_id = isset( $params['post_id'] ) ? (int) $params['post_id'] : 0;
$focus_keyword = isset( $params['focus_keyword'] )
? sanitize_text_field( $params['focus_keyword'] )
: '';
$title = isset( $params['title'] )
? sanitize_text_field( $params['title'] )
: '';
// Authorization: If linking to a post, check edit permission.
if ( $post_id > 0 && ! current_user_can( 'edit_post', $post_id ) ) {
return new WP_Error(
'forbidden',
__(
'You do not have permission to create a session for this post.',
'wp-agentic-writer',
),
[ 'status' => 403 ],
);
}
if ( '' === $title && $post_id > 0 ) {
$post = get_post( $post_id );
$base_title = $post ? sanitize_text_field( $post->post_title ) : '';
if ( '' === $base_title ) {
$base_title = 'Conversation';
}
$title = sprintf( '%s - %s', $base_title, current_time( 'Y-m-d H:i' ) );
}
$session_id = $manager->create_session(
[
'post_id' => $post_id,
'focus_keyword' => $focus_keyword,
'title' => $title,
]
);
if ( is_wp_error( $session_id ) ) {
return $session_id;
}
$session = $manager->get_session( $session_id );
// MEMANTO: New session created.
do_action(
'wpaw_memanto_session_start',
$session_id,
$post_id,
get_current_user_id(),
);
return new WP_REST_Response( $session, 201 );
}
/**
* Handle get single conversation request.
*
* @since 0.1.4
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error
*/
public function handle_get_conversation( $request ) {
$session_id = sanitize_text_field( $request->get_param( 'session_id' ) );
$manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
// Check authorization.
if ( ! $manager->current_user_can_access( $session_id ) ) {
return new WP_Error(
'forbidden',
__(
'You do not have permission to access this conversation.',
'wp-agentic-writer',
),
[ 'status' => 403 ],
);
}
$session = $manager->get_session( $session_id );
if ( ! $session ) {
return new WP_Error(
'not_found',
__( 'Conversation not found.', 'wp-agentic-writer' ),
[ 'status' => 404 ],
);
}
$session = $this->hydrate_session_plan_messages( $session );
return new WP_REST_Response( $session, 200 );
}
/**
* Restore rich plan UI payloads for sessions that only stored a text summary.
*
* @since 0.2.2
* @param array $session Conversation session.
* @return array
*/
private function hydrate_session_plan_messages( $session ) {
if ( ! is_array( $session ) ) {
return $session;
}
$post_id = isset( $session['post_id'] ) ? (int) $session['post_id'] : 0;
if (
$post_id <= 0
|| empty( $session['messages'] )
|| ! is_array( $session['messages'] )
) {
return $session;
}
foreach ( $session['messages'] as $message ) {
if (
isset( $message['type'] )
&& 'plan' === $message['type']
&& ! empty( $message['plan'] )
) {
return $session;
}
}
$plan = get_post_meta( $post_id, '_wpaw_plan', true );
if ( ! is_array( $plan ) ) {
return $session;
}
foreach ( $session['messages'] as $index => $message ) {
$content = isset( $message['content'] )
? (string) $message['content']
: '';
$role = isset( $message['role'] ) ? (string) $message['role'] : '';
if (
'assistant' !== $role
|| false === strpos( $content, 'Outline ready.' )
) {
continue;
}
$session['messages'][ $index ]['type'] = 'plan';
$session['messages'][ $index ]['plan'] = $plan;
break;
}
return $session;
}
/**
* Handle update conversation request.
*
* @since 0.1.4
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error
*/
public function handle_update_conversation( $request ) {
$params = $request->get_json_params();
$session_id = sanitize_text_field( $request->get_param( 'session_id' ) );
$manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
// Check authorization.
if ( ! $manager->current_user_can_access( $session_id ) ) {
return new WP_Error(
'forbidden',
__(
'You do not have permission to modify this conversation.',
'wp-agentic-writer',
),
[ 'status' => 403 ],
);
}
$session = $manager->get_session( $session_id );
if ( ! $session ) {
return new WP_Error(
'not_found',
__( 'Conversation not found.', 'wp-agentic-writer' ),
[ 'status' => 404 ],
);
}
// Update fields.
if ( isset( $params['title'] ) ) {
$manager->update_title( $session_id, $params['title'] );
}
if ( isset( $params['focus_keyword'] ) ) {
$manager->update_focus_keyword(
$session_id,
$params['focus_keyword'],
);
}
if ( isset( $params['status'] ) ) {
if ( 'completed' === $params['status'] ) {
$manager->mark_completed( $session_id );
// MEMANTO: Session completed.
$post_id = $session['post_id'] ?? 0;
do_action(
'wpaw_memanto_session_end',
$session_id,
(int) $post_id,
);
}
}
$updated_session = $manager->get_session( $session_id );
return new WP_REST_Response( $updated_session, 200 );
}
/**
* Handle delete conversation request.
*
* @since 0.1.4
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error
*/
public function handle_delete_conversation( $request ) {
$session_id = sanitize_text_field( $request->get_param( 'session_id' ) );
$manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
// Check authorization.
if ( ! $manager->current_user_can_access( $session_id ) ) {
return new WP_Error(
'forbidden',
__(
'You do not have permission to delete this conversation.',
'wp-agentic-writer',
),
[ 'status' => 403 ],
);
}
$result = $manager->delete_session( $session_id );
if ( ! $result ) {
return new WP_Error(
'delete_failed',
__( 'Failed to delete conversation.', 'wp-agentic-writer' ),
[ 'status' => 500 ],
);
}
// MEMANTO: Session deleted — treat as session end.
$session = $manager->get_session_unchecked( $session_id );
$post_id = $session ? $session['post_id'] ?? 0 : 0;
do_action( 'wpaw_memanto_session_end', $session_id, (int) $post_id );
return new WP_REST_Response( [ 'deleted' => true ], 200 );
}
/**
* Handle update conversation messages request.
*
* @since 0.1.4
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error
*/
public function handle_update_conversation_messages( $request ) {
$params = $request->get_json_params();
$session_id = sanitize_text_field( $request->get_param( 'session_id' ) );
$manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
// Check authorization.
if ( ! $manager->current_user_can_access( $session_id ) ) {
return new WP_Error(
'forbidden',
__(
'You do not have permission to modify this conversation.',
'wp-agentic-writer',
),
[ 'status' => 403 ],
);
}
$session = $manager->get_session( $session_id );
if ( ! $session ) {
return new WP_Error(
'not_found',
__( 'Conversation not found.', 'wp-agentic-writer' ),
[ 'status' => 404 ],
);
}
$messages = isset( $params['messages'] ) ? $params['messages'] : [];
if ( ! is_array( $messages ) ) {
return new WP_Error(
'invalid_messages',
__( 'Messages must be an array.', 'wp-agentic-writer' ),
[ 'status' => 400 ],
);
}
// Safety: refuse to overwrite existing messages with an empty array.
// This prevents race conditions during session switches from wiping
// conversation history. Intentional clears go through clear-context.
$existing_count = count( $session['messages'] ?? [] );
if ( empty( $messages ) && $existing_count > 0 ) {
return new WP_REST_Response(
[
'updated' => false,
'message_count' => $existing_count,
'skipped' => 'empty_overwrite_blocked',
],
200,
);
}
$updated = $manager->update_messages( $session_id, $messages );
if ( ! $updated ) {
return new WP_Error(
'message_update_failed',
__(
'Failed to update conversation messages.',
'wp-agentic-writer',
),
[ 'status' => 500 ],
);
}
return new WP_REST_Response(
[ 'updated' => true, 'message_count' => count( $messages ) ],
200,
);
}
/**
* Handle link conversation to post request.
*
* @since 0.1.4
* @param WP_REST_Request $request REST request.
* @return WP_REST_Response|WP_Error
*/
public function handle_link_conversation_to_post( $request ) {
$params = $request->get_json_params();
$session_id = sanitize_text_field( $request->get_param( 'session_id' ) );
$manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
// First verify user has access to this session (before linking to post).
if ( ! $manager->current_user_can_access( $session_id ) ) {
return new WP_Error(
'forbidden',
__(
'You do not have access to this conversation.',
'wp-agentic-writer',
),
[ 'status' => 403 ],
);
}
$session = $manager->get_session( $session_id );
if ( ! $session ) {
return new WP_Error(
'not_found',
__( 'Conversation not found.', 'wp-agentic-writer' ),
[ 'status' => 404 ],
);
}
$post_id = isset( $params['post_id'] ) ? (int) $params['post_id'] : 0;
if ( $post_id <= 0 ) {
return new WP_Error(
'invalid_post',
__( 'Valid post ID is required.', 'wp-agentic-writer' ),
[ 'status' => 400 ],
);
}
// Verify post exists and user can edit.
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return new WP_Error(
'permission_denied',
__(
'You do not have permission to edit this post.',
'wp-agentic-writer',
),
[ 'status' => 403 ],
);
}
$manager->link_to_post( $session_id, $post_id );
$updated_session = $manager->get_session( $session_id );
return new WP_REST_Response(
[
'linked' => true,
'post_id' => $post_id,
'session' => $updated_session,
],
200,
);
}
}