'; echo '' . esc_html__( 'Opace AI Scribe:', 'ai-scribe-the-chatgpt-powered-seo-content-creation-wizard' ) . ' ' . esc_html( sprintf( /* translators: %s: current PHP version. */ __( 'This plugin requires PHP 7.4 or higher. You are running PHP %s.', 'ai-scribe-the-chatgpt-powered-seo-content-creation-wizard' ), PHP_VERSION ) ); echo '

'; } ); return; } /* * The Opace AI Hub library is NOT bundled here. * * It used to be, as a fallback. Because the hub plugin's directory sorts * before this one, its copy always won and the bundled copy never ran — so * the two drifted apart unnoticed, and a provider fix applied to one of them * silently did nothing. That is exactly how Gemini shipped unable to generate * at all: the fix went into the copy that never loads. * * Opace AI Hub is a hard runtime dependency (see the guard below), so there is * one copy of the library, in the hub, and no way for a * fix to land in the wrong one. */ /** * Autoloader for AI_Scribe_* classes. * * v3 layout splits includes/ into core/, services/, adapters/ and ajax/ * (see REFACTOR.md section 6), so each subdirectory is searched in turn. */ spl_autoload_register( function ( $class_name ) { if ( strpos( $class_name, 'AI_Scribe_' ) !== 0 ) { return; } $class_file = 'class-' . str_replace( '_', '-', strtolower( substr( $class_name, 10 ) ) ) . '.php'; foreach ( array( 'core', 'adapters', 'services', 'ajax', 'abilities' ) as $subdir ) { $file_path = AI_SCRIBE_DIR . 'includes/' . $subdir . '/' . $class_file; if ( file_exists( $file_path ) ) { require_once $file_path; return; } } } ); /** * Main plugin bootstrap (singleton). Delegates everything to the * ServiceContainer via AI_Scribe_Plugin_Initializer. */ final class AI_Scribe { /** @var AI_Scribe|null */ private static $instance = null; /** @var AI_Scribe_Plugin_Initializer|null */ private $initializer = null; private function __construct() { try { $this->initializer = new AI_Scribe_Plugin_Initializer( AI_SCRIBE_FILE, AI_SCRIBE_VERSION ); // Global reference used by the AJAX handler service as a container fallback global $ai_scribe_plugin_initializer; $ai_scribe_plugin_initializer = $this->initializer; add_action( 'init', array( $this, 'load_textdomain' ) ); } catch ( Exception $e ) { add_action( 'admin_notices', function () use ( $e ) { echo '

'; echo '' . esc_html__( 'Opace AI Scribe:', 'ai-scribe-the-chatgpt-powered-seo-content-creation-wizard' ) . ' ' . esc_html( sprintf( /* translators: %s: plugin initialisation error message. */ __( 'Plugin initialisation failed: %s', 'ai-scribe-the-chatgpt-powered-seo-content-creation-wizard' ), $e->getMessage() ) ); echo '

'; } ); ai_scribe_debug_log( 'AI Scribe: initialisation failed: ' . $e->getMessage() ); } } public static function getInstance(): AI_Scribe { if ( self::$instance === null ) { self::$instance = new self(); } return self::$instance; } public function load_textdomain(): void { // Intentionally empty: translations load automatically from wp.org // language packs since WP 4.6 (text domain matches the plugin slug). } public function get_initializer() { return $this->initializer; } public function get_version(): string { return AI_SCRIBE_VERSION; } public function is_initialized(): bool { return $this->initializer && $this->initializer->is_initialized(); } } /** * Access the plugin instance. */ function ai_scribe(): AI_Scribe { return AI_Scribe::getInstance(); } /** * Access the global service container (used by services as a fallback). * * @return AI_Scribe_Service_Container|null */ function ai_scribe_get_container() { $plugin = ai_scribe(); if ( $plugin->get_initializer() ) { return $plugin->get_initializer()->get_container(); } return null; } /** * Hard dependency guard: no Opace AI Hub, no AI-Scribe. * * Detection is deliberately the single mechanism used everywhere else * (AI_Scribe_Onboarding_Notice::hub_active()). When the hub is missing or * deactivated we register one explanatory notice and stop: no admin menu, * no wizard, no AJAX endpoints, nothing half-rendered. The schema and * migration checks below are safe to skip because they re-run on the first * admin request after the hub comes back. */ // Front-end meta output for posts generated with no SEO plugin active // (C-2-4/L-26). Registered before the hub guard on purpose: already-saved // posts must keep their title/description even while Opace AI Hub is deactivated. AI_Scribe_Frontend_Meta::register(); // Generated posts carry a narrowly scoped reading-layout stylesheet. Theme // typography remains in charge; this only prevents edge-touching prose and // oversized images when a theme supplies no useful content defaults. add_action( 'wp_enqueue_scripts', static function () { if ( ! is_singular() || '1' !== (string) get_post_meta( get_queried_object_id(), '_ai_scribe_generated', true ) ) { return; } wp_enqueue_style( 'ai-scribe-article', AI_SCRIBE_URL . 'assets/css/frontend-article.css', array(), AI_SCRIBE_VERSION ); } ); if ( ! AI_Scribe_Onboarding_Notice::hub_active() ) { AI_Scribe_Onboarding_Notice::register_hub_setup(); return; } // Activation: conversations table + one-time 2.6.2 option migration register_activation_hook( __FILE__, function () { AI_Scribe_Conversation_Service::install_table(); $container = ai_scribe_get_container(); $prompt_manager = ( $container && $container->has( 'prompt_manager' ) ) ? $container->get( 'prompt_manager' ) : null; $config_manager = ( $container && $container->has( 'config' ) ) ? $container->get( 'config' ) : null; AI_Scribe_Migration_Service::maybe_migrate( $prompt_manager, $config_manager ); } ); // Upgrades without re-activation (updated in place): keep schema + options // current. Hooked on `init` rather than `admin_init` deliberately: admin_init // fires AFTER admin_menu, so on the first admin request following an update // (or an activation that could not run its hook) the menu and services would // otherwise build against a stale schema for that one request (C-14-1). add_action( 'init', function () { if ( ! is_admin() ) { return; } if ( get_option( 'ai_scribe_conversations_schema' ) !== AI_Scribe_Conversation_Service::SCHEMA_VERSION ) { AI_Scribe_Conversation_Service::install_table(); } if ( get_option( AI_Scribe_Migration_Service::MIGRATED_OPTION ) !== AI_Scribe_Migration_Service::MIGRATION_VERSION ) { $container = ai_scribe_get_container(); $prompt_manager = ( $container && $container->has( 'prompt_manager' ) ) ? $container->get( 'prompt_manager' ) : null; $config_manager = ( $container && $container->has( 'config' ) ) ? $container->get( 'config' ) : null; AI_Scribe_Migration_Service::maybe_migrate( $prompt_manager, $config_manager ); } } ); // Post-update onboarding + model-remap notices (REFACTOR.md §15.1/§15.2) AI_Scribe_Onboarding_Notice::register(); // Prompt Library tab: applying an Opace AI Hub prompt to a wizard step. AI_Scribe_Hub_Prompt_Reader::register(); // Boot ai_scribe();