false, 'message' => __( 'Please enter a question.', 'ait-ai-admin-assistance' ), ); } // Allow custom integration via filter (kept for compatibility with your ecosystem). $payload = array( 'question' => $question, 'context' => $context, ); $filtered = apply_filters( 'ait_aiaa_ai_response', null, $payload ); if ( is_array( $filtered ) && isset( $filtered['ok'] ) ) { return $filtered; } // Native OpenAI integration (Free) once settings are provided. $api_key = trim( (string) AIT_AIAA_Settings::get( 'openai_api_key' ) ); if ( '' === $api_key ) { return array( 'ok' => true, 'answer_html' => '

' . esc_html__( 'OpenAI is not configured yet.', 'ait-ai-admin-assistance' ) . '

' . esc_html__( 'Enter your OpenAI API key in Admin Guidance → Settings → AI Assistant.', 'ait-ai-admin-assistance' ) . '

', 'meta' => array(), ); } $model = trim( (string) AIT_AIAA_Settings::get( 'openai_model' ) ); if ( '' === $model ) { $model = 'gpt-4.1-mini'; } $temperature = (float) AIT_AIAA_Settings::get( 'openai_temperature' ); $max_out = (int) AIT_AIAA_Settings::get( 'openai_max_output_tokens' ); $system = $this->build_system_prompt( $context ); $user = $this->build_user_prompt( $question, $context ); $input = array( array( 'role' => 'system', 'content' => $system ), ); // Pro: optional conversation transcript messages (added only when user opts in). if ( ! empty( $context['conversation_messages'] ) && is_array( $context['conversation_messages'] ) ) { foreach ( $context['conversation_messages'] as $m ) { if ( ! is_array( $m ) ) { continue; } $role = isset( $m['role'] ) ? (string) $m['role'] : ''; $content = isset( $m['content'] ) ? (string) $m['content'] : ''; $role = in_array( $role, array( 'user', 'assistant' ), true ) ? $role : ''; $content = trim( $content ); if ( '' === $role || '' === $content ) { continue; } $input[] = array( 'role' => $role, 'content' => $content ); } } // Current question (with screen context) $input[] = array( 'role' => 'user', 'content' => $user ); $body = array( 'model' => $model, 'input' => $input, 'temperature' => $temperature, ); if ( ! empty( $max_out ) ) { $body['max_output_tokens'] = intval( $max_out ); } $start = microtime( true ); AIT_AIAA_Logger::log( 'openai_request', array( 'model' => $model, 'temperature' => $temperature, 'max_out' => ! empty( $max_out ) ? intval( $max_out ) : 0, 'language' => $language, 'input_count' => is_array( $input ) ? count( $input ) : 0, ) ); $response = wp_remote_post( 'https://api.openai.com/v1/responses', array( 'timeout' => 30, 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( $body ), ) ); if ( is_wp_error( $response ) ) { AIT_AIAA_Logger::log( 'openai_wp_error', array( 'error' => $response->get_error_message(), 'elapsed_ms' => (int) round( ( microtime( true ) - $start ) * 1000 ), ) ); return array( 'ok' => false, 'message' => $response->get_error_message(), ); } $code = (int) wp_remote_retrieve_response_code( $response ); $elapsed_ms = (int) round( ( microtime( true ) - $start ) * 1000 ); AIT_AIAA_Logger::log( 'openai_response', array( 'http_code' => $code, 'elapsed_ms' => $elapsed_ms, ) ); $raw = (string) wp_remote_retrieve_body( $response ); $data = json_decode( $raw, true ); if ( $code < 200 || $code >= 300 || ! is_array( $data ) ) { AIT_AIAA_Logger::log( 'openai_http_error', array( 'http_code' => $code, 'body' => $raw, ) ); $msg = __( 'OpenAI request failed.', 'ait-ai-admin-assistance' ); if ( is_array( $data ) && isset( $data['error']['message'] ) ) { $msg = sanitize_text_field( $data['error']['message'] ); } return array( 'ok' => false, 'message' => $msg ); } $text = $this->extract_text_from_responses_api( $data ); if ( '' === $text ) { return array( 'ok' => false, 'message' => __( 'No answer returned.', 'ait-ai-admin-assistance' ) ); } // Basic formatting. $answer_html = $this->format_answer_text_to_html( $text ); return array( 'ok' => true, 'answer_html' => $answer_html, 'meta' => array( 'model' => $model, ), ); } protected function extract_text_from_responses_api( $data ) { // Responses API usually returns output[] with content[] blocks. if ( isset( $data['output'] ) && is_array( $data['output'] ) ) { $out = ''; foreach ( $data['output'] as $item ) { if ( empty( $item['content'] ) || ! is_array( $item['content'] ) ) { continue; } foreach ( $item['content'] as $c ) { if ( isset( $c['type'] ) && 'output_text' === $c['type'] && isset( $c['text'] ) ) { $out .= (string) $c['text']; } elseif ( isset( $c['type'] ) && 'text' === $c['type'] && isset( $c['text'] ) ) { $out .= (string) $c['text']; } } } return trim( $out ); } // Fallbacks. if ( isset( $data['output_text'] ) ) { return trim( (string) $data['output_text'] ); } return ''; } /** * Convert lightweight markdown-ish output into safe HTML. * Special rule: numbered bold headings like "1. **Title**" become

Title

. */ protected function format_answer_text_to_html( $text ) { $text = (string) $text; $text = str_replace( array( "\r\n", "\r" ), "\n", $text ); $lines = explode( "\n", $text ); $out = array(); foreach ( $lines as $line ) { $raw = trim( $line ); if ( '' === $raw ) { $out[] = ''; continue; } // 1. **Heading** ->

Heading

if ( preg_match( '/^\d+\.\s+\*\*(.+?)\*\*\s*$/', $raw, $mm ) ) { $title = trim( $mm[1] ); if ( '' !== $title ) { $out[] = '

' . esc_html( $title ) . '

'; continue; } } // Inline conversions: **bold**, *italic*, `code` $html = $raw; // Code first (avoid interfering with asterisks inside code). $html = preg_replace_callback( '/`([^`]+)`/', function( $m ) { return '' . esc_html( $m[1] ) . ''; }, $html ); // Bold $html = preg_replace_callback( '/\*\*([^*]+)\*\*/', function( $m ) { return '' . esc_html( $m[1] ) . ''; }, $html ); // Italic (single asterisks), avoid matching inside tags. $html = preg_replace_callback( '/(^|\s)\*([^*]+)\*(\s|$)/', function( $m ) { return $m[1] . '' . esc_html( $m[2] ) . '' . $m[3]; }, $html ); // Escape any remaining raw HTML. // At this point, only our inserted tags should remain. $out[] = wp_kses_post( $html ); } $joined = implode( "\n", $out ); // wpautop will wrap plain lines into

and keep block tags like

intact. return wp_kses_post( wpautop( $joined ) ); } protected function build_system_prompt( $context ) { $role = isset( $context['user_role'] ) ? sanitize_text_field( $context['user_role'] ) : ''; $can_manage = ! empty( $context['can_manage_options'] ); $can_edit_posts = ! empty( $context['can_edit_posts'] ); $style = (string) AIT_AIAA_Settings::get( 'ai_response_style' ); $language = (string) AIT_AIAA_Settings::get( 'openai_default_language' ); $language = $language ? $language : 'auto'; $lines = array(); $lines[] = 'You are an admin assistant inside the WordPress dashboard.'; $lines[] = 'Answer the user question about the current admin screen and how to complete tasks safely.'; $lines[] = 'Be concise and step-by-step where helpful.'; $lines[] = 'Do not invent plugin features or settings. If unsure, say what to check in the UI.'; $lines[] = 'Avoid recommending direct code changes unless explicitly requested.'; if ( $language !== 'auto' ) { $opts = AIT_AIAA_Settings::get_language_options(); $lang_name = isset( $opts[ $language ] ) ? $opts[ $language ] : $language; $lines[] = 'Respond in ' . $lang_name . '.'; $lines[] = 'Keep WordPress UI labels in English when referring to menu items or screen labels that appear in the dashboard.'; } // Role shaping. if ( $role ) { $lines[] = 'The current user role is: ' . $role . '.'; } if ( ! $can_manage ) { $lines[] = 'Assume the user may NOT have administrator access (cannot manage options). Prefer editor-safe steps and mention when an action likely requires an admin.'; } if ( ! $can_edit_posts ) { $lines[] = 'Assume the user may have limited editing permissions. Offer general navigation help and ask them to contact an admin if they cannot access a menu.'; } if ( 'concise' === $style ) { $lines[] = 'Response style: concise.'; } elseif ( 'detailed' === $style ) { $lines[] = 'Response style: detailed.'; } else { $lines[] = 'Response style: balanced.'; } return implode( "\n", $lines ); } protected function build_user_prompt( $question, $context ) { $screen_id = isset( $context['screen_id'] ) ? sanitize_text_field( $context['screen_id'] ) : ''; $post_type = isset( $context['post_type'] ) ? sanitize_text_field( $context['post_type'] ) : ''; $taxonomy = isset( $context['taxonomy'] ) ? sanitize_text_field( $context['taxonomy'] ) : ''; $admin_path = isset( $context['admin_path'] ) ? sanitize_text_field( $context['admin_path'] ) : ''; $lines = array(); $lines[] = 'Current screen_id: ' . $screen_id; $lines[] = 'Current post_type: ' . $post_type; $lines[] = 'Current taxonomy: ' . $taxonomy; if ( $admin_path ) { $lines[] = 'Current admin path: ' . $admin_path; } // Designer guidance (screen + role). if ( ! empty( $context['guidance_text'] ) ) { $lines[] = '--- Designer guidance ---'; $lines[] = (string) $context['guidance_text']; } // Extra context (headings + field schema), only when enabled. if ( ! empty( $context['visible_headings'] ) && is_array( $context['visible_headings'] ) ) { $vh = $context['visible_headings']; $lines[] = '--- Visible headings & metabox titles ---'; if ( ! empty( $vh['h1'] ) ) { $lines[] = 'H1: ' . sanitize_text_field( $vh['h1'] ); } if ( ! empty( $vh['h2'] ) && is_array( $vh['h2'] ) ) { $lines[] = 'H2: ' . implode( ' | ', array_map( 'sanitize_text_field', $vh['h2'] ) ); } if ( ! empty( $vh['h3'] ) && is_array( $vh['h3'] ) ) { $lines[] = 'H3: ' . implode( ' | ', array_map( 'sanitize_text_field', $vh['h3'] ) ); } if ( ! empty( $vh['metabox_titles'] ) && is_array( $vh['metabox_titles'] ) ) { $lines[] = 'Metaboxes: ' . implode( ' | ', array_map( 'sanitize_text_field', $vh['metabox_titles'] ) ); } } if ( ! empty( $context['field_schema'] ) && is_array( $context['field_schema'] ) ) { $lines[] = '--- Form field schema (no values) ---'; $count = 0; foreach ( $context['field_schema'] as $row ) { if ( ! is_array( $row ) ) { continue; } $label = isset( $row['label'] ) ? sanitize_text_field( $row['label'] ) : ''; $type = isset( $row['type'] ) ? sanitize_text_field( $row['type'] ) : ''; $name = isset( $row['name'] ) ? sanitize_text_field( $row['name'] ) : ''; $lines[] = '- ' . ( $label ? $label : '(unlabeled)' ) . ' [type=' . $type . ' name=' . $name . ']'; $count++; if ( $count >= 80 ) { break; } } } // Optional site context. if ( AIT_AIAA_Settings::get( 'ai_include_site_context' ) ) { $lines[] = '--- Site context ---'; $lines[] = $this->get_site_context_summary(); } $lines[] = '--- Question ---'; $lines[] = $question; return implode( "\n", $lines ); } protected function get_site_context_summary() { $parts = array(); // Active theme. $theme = function_exists( 'wp_get_theme' ) ? wp_get_theme() : null; if ( $theme ) { $parts[] = 'Active theme: ' . sanitize_text_field( $theme->get( 'Name' ) ) . ' (' . sanitize_text_field( $theme->get_stylesheet() ) . ')'; } // Active plugins (names only). if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugins = get_plugins(); $active = (array) get_option( 'active_plugins', array() ); $active_names = array(); foreach ( $active as $file ) { if ( isset( $plugins[ $file ]['Name'] ) ) { $active_names[] = sanitize_text_field( $plugins[ $file ]['Name'] ); } } if ( ! empty( $active_names ) ) { $parts[] = 'Active plugins: ' . implode( ', ', $active_names ); } // Custom post types summary (public non-built-in). $cpts = get_post_types( array( '_builtin' => false ), 'objects' ); if ( ! empty( $cpts ) ) { $summaries = array(); foreach ( $cpts as $pt ) { $label = isset( $pt->labels->singular_name ) ? $pt->labels->singular_name : $pt->name; $summaries[] = $label . ' (key: ' . $pt->name . ', public: ' . ( $pt->public ? 'yes' : 'no' ) . ', has_archive: ' . ( $pt->has_archive ? 'yes' : 'no' ) . ')'; if ( count( $summaries ) >= 10 ) { break; } } $parts[] = 'Custom post types: ' . implode( ' | ', $summaries ); } return implode( "\n", $parts ); } }