';
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();