admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'abilities_bridge_nonce' ), 'user_id' => get_current_user_id(), 'attachments' => Abilities_Bridge_Attachments::get_config(), ) ); } /** * Render the main page */ public function render_page() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'abilities-bridge' ) ); } // Check if consent has been given. if ( ! Abilities_Bridge_Welcome_Wizard::is_setup_complete() ) { wp_safe_redirect( admin_url( 'admin.php?page=abilities-bridge-welcome' ) ); exit; } // Check if API key is configured. $provider_label = Abilities_Bridge_AI_Provider::get_provider_label(); $api_key_configured = Abilities_Bridge_AI_Provider::has_api_key(); ?>

add your API key to use Abilities Bridge.', 'abilities-bridge' ), esc_html( $provider_label ), esc_url( admin_url( 'admin.php?page=abilities-bridge-settings' ) ) ); ?>

__( 'Please complete the welcome wizard and provide consent before using Abilities Bridge.', 'abilities-bridge' ), 'redirect' => admin_url( 'admin.php?page=abilities-bridge-welcome' ), ) ); } // Register shutdown function to catch fatal errors. register_shutdown_function( function () { $error = error_get_last(); if ( $error && in_array( $error['type'], array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR ) ) ) { // Try to send JSON error response. if ( ! headers_sent() ) { wp_send_json_error( array( 'message' => 'Fatal error: ' . $error['message'], 'file' => basename( $error['file'] ), 'line' => $error['line'], ) ); } } } ); check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); } $message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : ''; $conversation_id = isset( $_POST['conversation_id'] ) ? intval( wp_unslash( $_POST['conversation_id'] ) ) : null; $plan_mode = isset( $_POST['plan_mode'] ) && sanitize_text_field( wp_unslash( $_POST['plan_mode'] ) ) === 'true'; $attachments_raw = isset( $_POST['attachments'] ) ? wp_unslash( $_POST['attachments'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $attachments = Abilities_Bridge_Attachments::parse_incoming_attachments( $attachments_raw ); if ( is_wp_error( $attachments ) ) { wp_send_json_error( array( 'message' => $attachments->get_error_message() ) ); } if ( '' === trim( $message ) && empty( $attachments ) ) { wp_send_json_error( array( 'message' => 'Message or image attachment required' ) ); } // Limit message length to prevent abuse. if ( strlen( $message ) > 10000 ) { wp_send_json_error( array( 'message' => 'Message too long (max 10,000 characters)' ) ); } if ( $conversation_id && ! $this->get_accessible_conversation( $conversation_id ) ) { wp_send_json_error( array( 'message' => 'Conversation not found' ) ); } // Load or create conversation. $conversation = new Abilities_Bridge_Conversation( $conversation_id ); if ( ! $conversation_id ) { // Create new conversation with first message as title. $title = Abilities_Bridge_Attachments::derive_title( $message, $attachments ); $new_id = $conversation->create( $title ); if ( ! $new_id ) { wp_send_json_error( array( 'message' => 'Failed to create conversation' ) ); } $conversation_id = $new_id; } $image_blocks = Abilities_Bridge_Attachments::store_attachments( $conversation_id, $attachments ); if ( is_wp_error( $image_blocks ) ) { wp_send_json_error( array( 'message' => $image_blocks->get_error_message() ) ); } // Log the action. Abilities_Bridge_Logger::log_action( $conversation_id, 'sent message to AI' ); // Log initial activity. Abilities_Bridge_Logger::log_tool_progress( $conversation_id, 'request', 'processing', '⏳ Processing message...' ); // Send message to Claude. $result = $conversation->send_message( $message, $plan_mode, $image_blocks ); if ( ! $result['success'] ) { Abilities_Bridge_Logger::log_action( $conversation_id, 'message failed', $result['error'] ); $error_payload = array( 'message' => $result['error'], 'error_data' => isset( $result['error_data'] ) ? $result['error_data'] : null, ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { $error_payload['debug'] = $result; } wp_send_json_error( $error_payload ); } // Check token usage. $token_usage = $conversation->calculate_token_usage(); // Check for summary continuation thresholds. $summary_status = 'none'; // Don't show warnings if this is a summary request. $is_summary_request = ( strpos( $message, 'comprehensive summary' ) !== false ); if ( ! $is_summary_request && isset( $token_usage['percentage'] ) ) { // Scale the warning to the model's context window so larger-window // models are not flagged prematurely. if ( $token_usage['percentage'] >= 90 ) { $summary_status = 'final_warning'; // Second warning - last chance. } elseif ( $token_usage['percentage'] >= 80 ) { $summary_status = 'first_warning'; // First warning - suggest summary. } } // Truncate response if too large for JSON. $response_data = array( 'response' => $result['response'], 'conversation_id' => $conversation_id, 'iterations' => $result['iterations'], 'tool_usage' => isset( $result['tool_usage'] ) ? $result['tool_usage'] : array(), 'token_usage' => $token_usage, 'summary_status' => $summary_status, ); // Check response size and truncate if needed. $json_size = strlen( wp_json_encode( $response_data ) ); if ( $json_size > 2097152 ) { // 2MB limit for JSON response. $response_data['response'] = substr( $result['response'], 0, 500000 ) . "\n\n[Response truncated due to size]"; $response_data['truncated'] = true; } wp_send_json_success( $response_data ); } /** * AJAX: Persist and dispatch a durable chat job. * * @throws RuntimeException When an enqueue checkpoint cannot be persisted. */ public function ajax_chat_job_enqueue() { $this->verify_chat_job_browser_request(); // The shared verifier above checks the nonce before any request field is read. // phpcs:disable WordPress.Security.NonceVerification.Missing $message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : ''; $conversation_id = isset( $_POST['conversation_id'] ) ? absint( wp_unslash( $_POST['conversation_id'] ) ) : 0; $plan_mode = isset( $_POST['plan_mode'] ) && 'true' === sanitize_text_field( wp_unslash( $_POST['plan_mode'] ) ); $attachments_raw = isset( $_POST['attachments'] ) ? wp_unslash( $_POST['attachments'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated by attachment parser. $supersede_token = isset( $_POST['supersede_job'] ) ? sanitize_text_field( wp_unslash( $_POST['supersede_job'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Missing $attachments = Abilities_Bridge_Attachments::parse_incoming_attachments( $attachments_raw ); if ( is_wp_error( $attachments ) ) { wp_send_json_error( array( 'message' => $attachments->get_error_message() ) ); } if ( '' === trim( $message ) && empty( $attachments ) ) { wp_send_json_error( array( 'message' => __( 'Message or image attachment required.', 'abilities-bridge' ) ) ); } if ( strlen( $message ) > 10000 ) { wp_send_json_error( array( 'message' => __( 'Message too long (max 10,000 characters).', 'abilities-bridge' ) ) ); } if ( $conversation_id && ! $this->get_accessible_conversation( $conversation_id ) ) { wp_send_json_error( array( 'message' => __( 'Conversation not found.', 'abilities-bridge' ) ) ); } $conversation = new Abilities_Bridge_Conversation( $conversation_id ); if ( ! $conversation_id ) { $conversation_id = (int) $conversation->create( Abilities_Bridge_Attachments::derive_title( $message, $attachments ) ); if ( ! $conversation_id ) { wp_send_json_error( array( 'message' => __( 'Failed to create conversation.', 'abilities-bridge' ) ) ); } } $locked = Abilities_Bridge_Chat_Jobs::acquire_conversation_lock( $conversation_id, 3 ); if ( ! $locked ) { wp_send_json_error( array( 'message' => __( 'Another chat request is being prepared. Please try again.', 'abilities-bridge' ) ) ); } $payload = null; $error = null; try { if ( '' !== $supersede_token ) { $superseded = Abilities_Bridge_Chat_Jobs::get_by_token( $supersede_token ); if ( $superseded && get_current_user_id() === (int) $superseded->user_id && $conversation_id === (int) $superseded->conversation_id ) { Abilities_Bridge_Chat_Jobs::request_cancel( $superseded->id ); } } $blocker = Abilities_Bridge_Chat_Jobs::find_blocking( $conversation_id ); if ( $blocker ) { $result = Abilities_Bridge_Chat_Jobs::sweep( $blocker->id ); if ( 'requeued' === $result ) { Abilities_Bridge_Chat_Job_Runner::dispatch( $blocker->id, true ); } $blocker = Abilities_Bridge_Chat_Jobs::find_blocking( $conversation_id ); } if ( $blocker ) { throw new RuntimeException( __( 'This conversation already has a question in progress.', 'abilities-bridge' ) ); } $conversation_data = Abilities_Bridge_Database::get_conversation( $conversation_id, get_current_user_id() ); $provider = ! empty( $conversation_data->provider ) ? $conversation_data->provider : Abilities_Bridge_AI_Provider::get_current_provider(); $model = ! empty( $conversation_data->model ) ? $conversation_data->model : Abilities_Bridge_AI_Provider::get_selected_model( $provider ); $job = Abilities_Bridge_Chat_Jobs::create_preparing( array( 'conversation_id' => $conversation_id, 'user_id' => get_current_user_id(), 'provider' => $provider, 'model' => $model, 'plan_mode' => $plan_mode, ) ); if ( is_wp_error( $job ) ) { throw new RuntimeException( $job->get_error_message() ); } $image_blocks = Abilities_Bridge_Attachments::store_attachments( $conversation_id, $attachments ); if ( is_wp_error( $image_blocks ) ) { Abilities_Bridge_Chat_Jobs::delete_preparing( $job->id ); throw new RuntimeException( $image_blocks->get_error_message() ); } if ( ! Abilities_Bridge_Chat_Jobs::store_preparing_attachments( $job->id, wp_json_encode( $image_blocks ) ) ) { Abilities_Bridge_Attachments::delete_attachment_blocks( $conversation_id, $image_blocks ); Abilities_Bridge_Chat_Jobs::delete_preparing( $job->id ); throw new RuntimeException( __( 'The attachment checkpoint could not be saved.', 'abilities-bridge' ) ); } $processor = new Abilities_Bridge_Message_Processor(); $message_id = $processor->begin( $conversation, $message, $plan_mode, $image_blocks, $job->id ); if ( is_wp_error( $message_id ) ) { Abilities_Bridge_Attachments::delete_attachment_blocks( $conversation_id, $image_blocks ); Abilities_Bridge_Chat_Jobs::delete_preparing( $job->id ); throw new RuntimeException( $message_id->get_error_message() ); } if ( ! Abilities_Bridge_Chat_Jobs::activate_prepared( $job->id, $message_id, wp_json_encode( $image_blocks ) ) ) { Abilities_Bridge_Database::delete_message( $message_id, $conversation_id ); Abilities_Bridge_Attachments::delete_attachment_blocks( $conversation_id, $image_blocks ); Abilities_Bridge_Chat_Jobs::delete_preparing( $job->id ); throw new RuntimeException( __( 'The chat job could not be queued safely.', 'abilities-bridge' ) ); } Abilities_Bridge_Chat_Jobs::mark_superseded( $conversation_id, $job->id ); Abilities_Bridge_Logger::log_action( $conversation_id, 'queued message for AI' ); Abilities_Bridge_Chat_Job_Runner::dispatch( $job->id, true ); $payload = array( 'job' => $job->public_token, 'conversation_id' => $conversation_id, 'status' => 'queued', ); } catch ( Throwable $throwable ) { $error = $throwable->getMessage(); } finally { Abilities_Bridge_Chat_Jobs::release_conversation_lock( $conversation_id ); } if ( null !== $error ) { wp_send_json_error( array( 'message' => $error ) ); } wp_send_json_success( $payload ); } /** * AJAX: Poll one durable job. */ public function ajax_chat_job_status() { $this->verify_chat_job_browser_request(); $job = $this->get_owned_job_from_request(); $sweep = Abilities_Bridge_Chat_Jobs::sweep( $job->id ); $job = Abilities_Bridge_Chat_Jobs::get( $job->id ); if ( Abilities_Bridge_Chat_Jobs::is_openai_recovery_pending( $job ) ) { Abilities_Bridge_Chat_Job_Runner::recover_openai_job( $job ); $job = Abilities_Bridge_Chat_Jobs::get( $job->id ); } if ( $job && 'queued' === $job->status && ( 'requeued' === $sweep || Abilities_Bridge_Chat_Jobs::age( $job ) >= 15 ) ) { Abilities_Bridge_Chat_Job_Runner::dispatch( $job->id ); $job = Abilities_Bridge_Chat_Jobs::get( $job->id ); } wp_send_json_success( $this->format_chat_job( $job ) ); } /** * AJAX: Stop a job immediately and free the conversation for another turn. */ public function ajax_chat_job_cancel() { $this->verify_chat_job_browser_request(); $job = $this->get_owned_job_from_request(); $conversation_id = (int) $job->conversation_id; if ( ! Abilities_Bridge_Chat_Jobs::acquire_conversation_lock( $conversation_id, 2 ) ) { wp_send_json_error( array( 'message' => __( 'The conversation is busy finalizing a response. Please try Stop again.', 'abilities-bridge' ) ), 409 ); } try { $job = Abilities_Bridge_Chat_Jobs::request_cancel( $job->id ); } finally { Abilities_Bridge_Chat_Jobs::release_conversation_lock( $conversation_id ); } wp_send_json_success( $this->format_chat_job( $job ) ); } /** * AJAX: Find a resumable job for a conversation after page load. */ public function ajax_chat_job_resume() { $this->verify_chat_job_browser_request(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified by shared guard above. $conversation_id = isset( $_POST['conversation_id'] ) ? absint( wp_unslash( $_POST['conversation_id'] ) ) : 0; if ( $conversation_id && ! $this->get_accessible_conversation( $conversation_id ) ) { wp_send_json_success( array( 'job' => null ) ); } $job = $conversation_id ? Abilities_Bridge_Chat_Jobs::find_resumable( $conversation_id, get_current_user_id() ) : Abilities_Bridge_Chat_Jobs::find_latest_resumable_for_user( get_current_user_id() ); if ( $job && ! $this->get_accessible_conversation( $job->conversation_id ) ) { $job = null; } if ( $job ) { Abilities_Bridge_Chat_Jobs::sweep( $job->id ); $job = Abilities_Bridge_Chat_Jobs::get( $job->id ); if ( Abilities_Bridge_Chat_Jobs::is_openai_recovery_pending( $job ) ) { Abilities_Bridge_Chat_Job_Runner::recover_openai_job( $job ); $job = Abilities_Bridge_Chat_Jobs::get( $job->id ); } if ( 'queued' === $job->status ) { Abilities_Bridge_Chat_Job_Runner::dispatch( $job->id ); } } wp_send_json_success( array( 'job' => $job ? $this->format_chat_job( $job ) : null ) ); } /** * AJAX: Explicit foreground transport takeover of the same prepared job. */ public function ajax_chat_job_run_foreground() { $this->verify_chat_job_browser_request(); $job = $this->get_owned_job_from_request(); $runner = new Abilities_Bridge_Chat_Job_Runner(); $result = $runner->run_existing( $job->id, $job->runner_token ); $job = Abilities_Bridge_Chat_Jobs::get( $job->id ); if ( is_wp_error( $result ) && ( ! $job || 'queued' === $job->status ) ) { wp_send_json_error( array( 'message' => $result->get_error_message(), 'job' => $this->format_chat_job( $job ), ) ); } wp_send_json_success( $this->format_chat_job( $job ) ); } /** * Verify the common browser-facing chat-job security boundary. */ private function verify_chat_job_browser_request() { check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'abilities-bridge' ) ), 403 ); } if ( ! Abilities_Bridge_Welcome_Wizard::is_setup_complete() ) { wp_send_json_error( array( 'message' => __( 'Please complete the welcome wizard before using chat.', 'abilities-bridge' ) ), 403 ); } } /** * Resolve a public token and enforce user ownership. * * @return object Job row. */ private function get_owned_job_from_request() { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- every caller runs the shared verifier first. $token = isset( $_POST['job'] ) ? sanitize_text_field( wp_unslash( $_POST['job'] ) ) : ''; $job = Abilities_Bridge_Chat_Jobs::get_by_token( $token ); if ( ! $job || get_current_user_id() !== (int) $job->user_id || ! $this->get_accessible_conversation( $job->conversation_id ) ) { wp_send_json_error( array( 'message' => __( 'Chat job not found.', 'abilities-bridge' ) ), 404 ); } return $job; } /** * Format safe progress data for browser polling. * * @param object $job Job row. * @return array */ private function format_chat_job( $job ) { if ( ! $job ) { return array( 'status' => 'missing' ); } return array( 'job' => $job->public_token, 'conversation_id' => (int) $job->conversation_id, 'status' => $job->status, 'phase' => $job->phase, 'rounds_completed' => (int) $job->rounds_completed, 'last_activity' => sanitize_text_field( $job->last_activity ), 'elapsed' => Abilities_Bridge_Chat_Jobs::age( $job ), 'error_code' => sanitize_key( $job->error_code ), 'error_message' => sanitize_text_field( $job->error_message ), 'background_unavailable' => 'queued' === $job->status && Abilities_Bridge_Chat_Jobs::age( $job ) >= 45, 'cancel_requested' => (bool) $job->cancel_requested, 'recovery_pending' => Abilities_Bridge_Chat_Jobs::is_openai_recovery_pending( $job ), ); } /** * AJAX: Serve a private conversation image attachment. * * @return void */ public function ajax_get_attachment() { check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { status_header( 403 ); wp_die( esc_html__( 'Insufficient permissions', 'abilities-bridge' ) ); } $conversation_id = isset( $_GET['conversation_id'] ) ? intval( wp_unslash( $_GET['conversation_id'] ) ) : 0; $attachment_id = isset( $_GET['attachment_id'] ) ? sanitize_key( wp_unslash( $_GET['attachment_id'] ) ) : ''; if ( ! $conversation_id || ! $attachment_id || ! $this->get_accessible_conversation( $conversation_id ) ) { status_header( 404 ); wp_die( esc_html__( 'Image attachment not found.', 'abilities-bridge' ) ); } $attachment = Abilities_Bridge_Attachments::get_attachment_for_serving( $conversation_id, $attachment_id ); if ( is_wp_error( $attachment ) ) { status_header( 404 ); wp_die( esc_html( $attachment->get_error_message() ) ); } nocache_headers(); header( 'Content-Type: ' . $attachment['mime_type'] ); header( 'Content-Length: ' . filesize( $attachment['path'] ) ); header( 'Content-Disposition: inline; filename="' . sanitize_file_name( $attachment['name'] ) . '"' ); readfile( $attachment['path'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile exit; } /** * AJAX: Create new conversation */ public function ajax_new_conversation() { check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); } // Check if consent has been given. if ( ! Abilities_Bridge_Welcome_Wizard::is_setup_complete() ) { wp_send_json_error( array( 'message' => __( 'Please complete the welcome wizard and provide consent before using Abilities Bridge.', 'abilities-bridge' ), 'redirect' => admin_url( 'admin.php?page=abilities-bridge-welcome' ), ) ); } wp_send_json_success( array( 'message' => 'New conversation started', ) ); } /** * AJAX: Load conversation */ public function ajax_load_conversation() { check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); } // Check if consent has been given. if ( ! Abilities_Bridge_Welcome_Wizard::is_setup_complete() ) { wp_send_json_error( array( 'message' => __( 'Please complete the welcome wizard and provide consent before using Abilities Bridge.', 'abilities-bridge' ), 'redirect' => admin_url( 'admin.php?page=abilities-bridge-welcome' ), ) ); } $conversation_id = isset( $_POST['conversation_id'] ) ? intval( wp_unslash( $_POST['conversation_id'] ) ) : 0; if ( ! $conversation_id ) { wp_send_json_error( array( 'message' => 'Invalid conversation ID' ) ); } $conversation = $this->get_accessible_conversation( $conversation_id ); if ( ! $conversation ) { wp_send_json_error( array( 'message' => 'Conversation not found' ) ); } $messages = Abilities_Bridge_Database::get_messages( $conversation_id ); $formatted_messages = array(); foreach ( $messages as $msg ) { $content = json_decode( $msg->content, true ); // Only include user and text assistant messages for display. if ( 'user' === $msg->role ) { // Check if this is a tool_result message (should be hidden from UI). if ( is_array( $content ) && isset( $content[0]['type'] ) && 'tool_result' === $content[0]['type'] ) { // Skip tool_result messages - these are internal Claude API messages. continue; } if ( null === $content || is_string( $content ) ) { // Plain text message - display it. $formatted_messages[] = array( 'role' => 'user', 'content' => $msg->content, 'timestamp' => $msg->created_at, ); } elseif ( is_array( $content ) ) { $formatted_messages[] = array( 'role' => 'user', 'content' => Abilities_Bridge_Attachments::format_content_for_display( $content, $conversation_id ), 'timestamp' => $msg->created_at, ); } } elseif ( 'assistant' === $msg->role ) { // Superseded/cancelled turns remain auditable in storage, but their // partial model output must not appear as part of the newer exchange. if ( isset( $msg->context_visible ) && ! (int) $msg->context_visible ) { continue; } if ( is_array( $content ) ) { // Extract text from content blocks. $text_parts = array(); foreach ( $content as $block ) { if ( isset( $block['type'] ) && 'text' === $block['type'] ) { $text_parts[] = $block['text']; } } if ( ! empty( $text_parts ) ) { $formatted_messages[] = array( 'role' => 'assistant', 'content' => implode( "\n\n", $text_parts ), 'timestamp' => $msg->created_at, ); } } else { $formatted_messages[] = array( 'role' => 'assistant', 'content' => $msg->content, 'timestamp' => $msg->created_at, ); } } } wp_send_json_success( array( 'conversation' => $conversation, 'messages' => $formatted_messages, ) ); } /** * AJAX: Delete conversation */ public function ajax_delete_conversation() { check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); } $conversation_id = isset( $_POST['conversation_id'] ) ? intval( wp_unslash( $_POST['conversation_id'] ) ) : 0; if ( ! $conversation_id ) { wp_send_json_error( array( 'message' => 'Invalid conversation ID' ) ); } $conversation = $this->get_accessible_conversation( $conversation_id ); if ( ! $conversation ) { wp_send_json_error( array( 'message' => 'Conversation not found' ) ); } $result = Abilities_Bridge_Database::delete_conversation( $conversation_id, get_current_user_id() ); if ( $result ) { wp_send_json_success( array( 'message' => 'Conversation deleted' ) ); } else { wp_send_json_error( array( 'message' => 'Failed to delete conversation' ) ); } } /** * AJAX: Get conversations list */ public function ajax_get_conversations() { check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); } $user_id = get_current_user_id(); $conversations = Abilities_Bridge_Database::get_conversations( $user_id ); // Ensure conversations is always an array. if ( ! is_array( $conversations ) ) { $conversations = array(); } wp_send_json_success( array( 'conversations' => $conversations, ) ); } /** * AJAX: Get token usage for current conversation */ public function ajax_get_token_usage() { check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); } $conversation_id = isset( $_POST['conversation_id'] ) ? intval( wp_unslash( $_POST['conversation_id'] ) ) : 0; if ( $conversation_id ) { if ( ! $this->get_accessible_conversation( $conversation_id ) ) { wp_send_json_error( array( 'message' => 'Conversation not found' ) ); } $conversation = new Abilities_Bridge_Conversation( $conversation_id ); $token_usage = $conversation->calculate_token_usage(); wp_send_json_success( $token_usage ); } else { // Return empty token usage for no conversation. wp_send_json_success( array( 'messages' => 0, 'system' => 0, 'tools' => 0, 'total' => 0, 'model' => 'claude-sonnet-4.5-20250929', 'input_limit' => 200000, 'output_limit' => 64000, 'percentage' => 0, 'status' => 'no_conversation', ) ); } } /** * AJAX: Get recent activity (for real-time progress polling) */ public function ajax_get_recent_activity() { check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); } // Close session to allow concurrent requests (enables real-time progress polling). if ( session_status() === PHP_SESSION_ACTIVE ) { session_write_close(); } $conversation_id = isset( $_POST['conversation_id'] ) ? intval( wp_unslash( $_POST['conversation_id'] ) ) : 0; if ( empty( $conversation_id ) ) { wp_send_json_error( array( 'message' => 'Conversation ID required' ) ); } if ( ! $this->get_accessible_conversation( $conversation_id ) ) { wp_send_json_error( array( 'message' => 'Conversation not found' ) ); } $logs = Abilities_Bridge_Logger::get_recent_activity( $conversation_id, 30 ); $activities = array(); foreach ( $logs as $log ) { $activities[] = array( 'message' => $log->action, 'function_name' => $log->function_name, 'timestamp' => $log->created_at, ); } wp_send_json_success( array( 'activities' => $activities ) ); } /** * AJAX: Get all conversation activity (for activity history dropdown) */ public function ajax_get_conversation_activity() { check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); } $conversation_id = isset( $_POST['conversation_id'] ) ? intval( wp_unslash( $_POST['conversation_id'] ) ) : 0; if ( empty( $conversation_id ) ) { wp_send_json_error( array( 'message' => 'Conversation ID required' ) ); } if ( ! $this->get_accessible_conversation( $conversation_id ) ) { wp_send_json_error( array( 'message' => 'Conversation not found' ) ); } $logs = Abilities_Bridge_Logger::get_conversation_activity( $conversation_id ); $activities = array(); foreach ( $logs as $log ) { $input = $log->function_input ? json_decode( $log->function_input, true ) : null; $activities[] = array( 'timestamp' => $log->created_at, 'function' => $log->function_name, 'description' => $log->action, 'input' => $input, 'success' => empty( $log->error_message ), 'error' => $log->error_message, ); } wp_send_json_success( array( 'activities' => $activities, 'count' => count( $activities ), ) ); } /** * AJAX: Set selected model */ public function ajax_set_model() { check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); } $model = isset( $_POST['model'] ) ? sanitize_text_field( wp_unslash( $_POST['model'] ) ) : ''; if ( empty( $model ) ) { wp_send_json_error( array( 'message' => 'Model is required' ) ); } $provider = Abilities_Bridge_AI_Provider::get_current_provider(); $success = Abilities_Bridge_AI_Provider::set_selected_model( $model, $provider ); if ( $success ) { $available_models = Abilities_Bridge_AI_Provider::get_available_models( $provider ); wp_send_json_success( array( 'message' => 'Model updated successfully', 'model' => $model, 'model_name' => isset( $available_models[ $model ] ) ? $available_models[ $model ] : $model, 'model_guidance' => Abilities_Bridge_AI_Provider::get_model_guidance( $model, $provider ), ) ); } else { wp_send_json_error( array( 'message' => sprintf( 'Invalid model for %s provider', Abilities_Bridge_AI_Provider::get_provider_label( $provider ) ), ) ); } } /** * AJAX: Get selected model */ public function ajax_get_model() { check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); } $provider = Abilities_Bridge_AI_Provider::get_current_provider(); $model = Abilities_Bridge_AI_Provider::get_selected_model( $provider ); $available_models = Abilities_Bridge_AI_Provider::get_available_models( $provider ); wp_send_json_success( array( 'provider' => $provider, 'model' => $model, 'model_name' => isset( $available_models[ $model ] ) ? $available_models[ $model ] : $model, 'model_guidance' => Abilities_Bridge_AI_Provider::get_model_guidance( $model, $provider ), 'available_models' => $available_models, ) ); } /** * AJAX: Set selected provider */ public function ajax_set_provider() { check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); } $provider = isset( $_POST['provider'] ) ? sanitize_text_field( wp_unslash( $_POST['provider'] ) ) : ''; if ( empty( $provider ) ) { wp_send_json_error( array( 'message' => 'Provider is required' ) ); } $success = Abilities_Bridge_AI_Provider::set_selected_provider( $provider ); if ( $success ) { $available_models = Abilities_Bridge_AI_Provider::get_available_models( $provider ); $model = Abilities_Bridge_AI_Provider::get_selected_model( $provider ); wp_send_json_success( array( 'message' => 'Provider updated successfully', 'provider' => $provider, 'provider_label' => Abilities_Bridge_AI_Provider::get_provider_label( $provider ), 'model' => $model, 'model_name' => isset( $available_models[ $model ] ) ? $available_models[ $model ] : $model, 'model_guidance' => Abilities_Bridge_AI_Provider::get_model_guidance( $model, $provider ), 'available_models' => $available_models, ) ); } else { wp_send_json_error( array( 'message' => 'Invalid provider' ) ); } } /** * AJAX: Get selected provider */ public function ajax_get_provider() { check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); } $provider = Abilities_Bridge_AI_Provider::get_current_provider(); $available_models = Abilities_Bridge_AI_Provider::get_available_models( $provider ); $model = Abilities_Bridge_AI_Provider::get_selected_model( $provider ); wp_send_json_success( array( 'provider' => $provider, 'provider_label' => Abilities_Bridge_AI_Provider::get_provider_label( $provider ), 'model' => $model, 'model_name' => isset( $available_models[ $model ] ) ? $available_models[ $model ] : $model, 'model_guidance' => Abilities_Bridge_AI_Provider::get_model_guidance( $model, $provider ), 'available_models' => $available_models, ) ); } /** * AJAX: Create summary continuation conversation * * Creates a new conversation with the summary as the first user message, * linking it to the parent conversation via parent_conversation_id. */ public function ajax_create_summary_continuation() { check_ajax_referer( 'abilities_bridge_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Insufficient permissions' ) ); } $parent_conversation_id = isset( $_POST['parent_conversation_id'] ) ? intval( wp_unslash( $_POST['parent_conversation_id'] ) ) : 0; $summary_text = isset( $_POST['summary_text'] ) ? sanitize_textarea_field( wp_unslash( $_POST['summary_text'] ) ) : ''; if ( ! $parent_conversation_id ) { wp_send_json_error( array( 'message' => 'Invalid parent conversation ID' ) ); } if ( empty( $summary_text ) ) { wp_send_json_error( array( 'message' => 'Summary text is required' ) ); } // Get parent conversation details. $parent_conversation = $this->get_accessible_conversation( $parent_conversation_id ); if ( ! $parent_conversation ) { wp_send_json_error( array( 'message' => 'Parent conversation not found' ) ); } // Create new conversation with same title and model. global $wpdb; $result = $wpdb->insert( Abilities_Bridge_Database::table( Abilities_Bridge_Database::TABLE_CONVERSATIONS ), array( 'user_id' => get_current_user_id(), 'title' => sanitize_text_field( $parent_conversation->title ), // Keep same title. 'provider' => ! empty( $parent_conversation->provider ) ? sanitize_text_field( $parent_conversation->provider ) : Abilities_Bridge_AI_Provider::get_current_provider(), 'model' => sanitize_text_field( $parent_conversation->model ), 'parent_conversation_id' => $parent_conversation_id, ), array( '%d', '%s', '%s', '%s', '%d' ) ); if ( ! $result ) { wp_send_json_error( array( 'message' => 'Failed to create new conversation' ) ); } $new_conversation_id = $wpdb->insert_id; // Add summary as first user message in new conversation. Abilities_Bridge_Database::add_message( $new_conversation_id, 'user', $summary_text ); wp_send_json_success( array( 'new_conversation_id' => $new_conversation_id, 'parent_conversation_id' => $parent_conversation_id, 'message' => 'Summary continuation created successfully', ) ); } }