false, 'behavior' => false, 'embedding_model' => false, 'knowledge_base' => false, 'actions' => false, ); $stored = get_option('mxchat_onboarding_progress', array()); if (!is_array($stored)) { $stored = array(); } $out = array(); foreach ($defaults as $k => $v) { $out[$k] = isset($stored[$k]) ? (bool) $stored[$k] : $v; } return $out; } /** * Update one or more wizard progress flags. Merges into the stored array. */ function mxchat_onboarding_set_progress($changes) { $current = mxchat_onboarding_get_progress(); foreach ((array) $changes as $k => $v) { if (array_key_exists($k, $current)) { $current[$k] = (bool) $v; } } update_option('mxchat_onboarding_progress', $current); return $current; } /** * Existing AJAX: user-initiated dismiss. Preserved verbatim from f7c7d4. */ add_action('wp_ajax_mxchat_dismiss_onboarding', 'mxchat_handle_dismiss_onboarding'); function mxchat_handle_dismiss_onboarding() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission to do this.', 'mxchat')), 403); } check_ajax_referer('mxchat_dismiss_onboarding', 'nonce'); update_option('mxchat_onboarding_dismissed', 1); update_option('mxchat_onboarding_auto_graduated', 0); wp_send_json_success(array('dismissed' => true)); } /** * admin_post: "Show MxChat Onboarding again" — clears both dismissed flag * AND wizard progress so the user gets a fresh wizard. Preserved from f7c7d4 * with the added progress-reset. */ add_action('admin_post_mxchat_unhide_onboarding', 'mxchat_handle_unhide_onboarding'); function mxchat_handle_unhide_onboarding() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to do this.', 'mxchat')); } check_admin_referer('mxchat_unhide_onboarding'); delete_option('mxchat_onboarding_dismissed'); delete_option('mxchat_onboarding_auto_graduated'); wp_safe_redirect(admin_url('admin.php?page=mxchat-onboarding')); exit; } /** * admin_init: legacy slug 301 redirect (preserved from f7c7d4). */ add_action('admin_init', 'mxchat_onboarding_legacy_slug_redirect'); function mxchat_onboarding_legacy_slug_redirect() { if (!isset($_GET['page'])) { return; } if ($_GET['page'] !== 'mxchat-dashboard') { return; } wp_safe_redirect(admin_url('admin.php?page=mxchat-onboarding'), 301); exit; } /** * Provider catalog used by Steps 1 and 2. Wraps the canonical catalog in * includes/class-mxchat-model-catalog.php (plan-d14e89) so a single edit * there flows through to the Onboarding wizard's dropdowns. Shape preserved * for back-compat: * [ slug => [ label, key_option, chat_models{}, embedding_models{} ] ] * Plus an extra `requires_key_to_load_models` flag per provider (true for * OpenRouter) so the wizard can disable that provider in its picker. */ function mxchat_onboarding_provider_catalog() { if (!class_exists('MxChat_Model_Catalog')) { require_once plugin_dir_path(__FILE__) . 'class-mxchat-model-catalog.php'; } $chat = MxChat_Model_Catalog::chat_models(); $emb = MxChat_Model_Catalog::embedding_models(); $out = array(); $all_slugs = array_unique(array_merge(array_keys($chat), array_keys($emb))); foreach ($all_slugs as $slug) { $chat_models = array(); $requires_key = false; $disable_onb = false; if (isset($chat[$slug])) { foreach ($chat[$slug]['models'] as $id => $entry) { $chat_models[$id] = $entry['label']; } $requires_key = !empty($chat[$slug]['requires_key_to_load_models']); $disable_onb = !empty($chat[$slug]['disable_in_onboarding']); } $embed_models = array(); if (isset($emb[$slug])) { foreach ($emb[$slug]['models'] as $id => $entry) { $embed_models[$id] = $entry['label']; } } $out[$slug] = array( 'label' => MxChat_Model_Catalog::provider_label($slug), 'key_option' => MxChat_Model_Catalog::key_option_for_provider($slug), 'chat_models' => $chat_models, 'embedding_models' => $embed_models, 'requires_key_to_load_models' => $requires_key, 'disable_in_onboarding' => ($disable_onb || $requires_key), ); } return $out; } /** * Infer the provider slug from a chat model id, mirroring the rule of thumb * the model picker uses. Returns 'openai' / 'claude' / 'gemini' / 'xai' / * 'deepseek' / 'openrouter' / 'custom' or '' if unknown. */ function mxchat_onboarding_provider_from_model($model_id) { $m = strtolower((string) $model_id); if ($m === '') return ''; if (strpos($m, 'gpt-') === 0 || strpos($m, 'o1-') === 0 || strpos($m, 'o3-') === 0) return 'openai'; if (strpos($m, 'claude-') === 0) return 'claude'; if (strpos($m, 'gemini-') === 0) return 'gemini'; if (strpos($m, 'grok') === 0) return 'xai'; if (strpos($m, 'deepseek') === 0) return 'deepseek'; if (strpos($m, 'sonar') === 0 || strpos($m, 'pplx-') === 0) return 'perplexity'; if ($m === 'openrouter' || strpos($m, '/') !== false) return 'openrouter'; if (strpos($m, 'custom') === 0) return 'custom'; return ''; } /** * Mirror for embedding models. */ function mxchat_onboarding_provider_from_embedding_model($model_id) { $m = strtolower((string) $model_id); if ($m === '') return ''; if (strpos($m, 'text-embedding-') === 0) return 'openai'; if (strpos($m, 'voyage') === 0) return 'voyage'; if (strpos($m, 'gemini-embedding') === 0) return 'gemini'; return ''; } /** * Whether the given provider has a non-empty API key saved. */ function mxchat_onboarding_provider_has_key($provider_slug) { $catalog = mxchat_onboarding_provider_catalog(); if (!isset($catalog[$provider_slug])) return false; $opts = get_option('mxchat_options', array()); if (!is_array($opts)) $opts = array(); $field = $catalog[$provider_slug]['key_option']; return !empty($opts[$field]) && trim((string) $opts[$field]) !== ''; } /** * KB row count. The canonical KB content table is * {$wpdb->prefix}mxchat_system_prompt_content — see mxchat-basic.php:716 * where the table is created. An earlier draft of this function queried * mxchat_embeddings (which doesn't exist on most installs), causing Step 3 * to always report "No knowledge base items yet" even after items had been * added. Plan-d14e89 Issue 6. */ function mxchat_onboarding_kb_count() { global $wpdb; $table = $wpdb->prefix . 'mxchat_system_prompt_content'; $exists = (bool) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)); if (!$exists) return 0; return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); } /** * AJAX: live KB status poll for Step 3. Returns { count: N }. * Nonce-protected per plan spec (use per-request nonce path from 6a68c9 — * here the nonce is issued fresh in the page renderer, NOT cached, since * admin pages aren't page-cached). */ add_action('wp_ajax_mxchat_onboarding_kb_status', 'mxchat_onboarding_ajax_kb_status'); function mxchat_onboarding_ajax_kb_status() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission.', 'mxchat')), 403); } check_ajax_referer('mxchat_onboarding_nonce', 'nonce'); wp_send_json_success(array('count' => mxchat_onboarding_kb_count())); } /** * AJAX: save Step 1 (chat) or Step 2 (embedding) selection. Persists * provider/model/key into the SAME mxchat_options sub-keys Settings uses * (no parallel storage), then flips the corresponding progress flag. * * Expected POST: which=chat|embedding, provider=, model=, * api_key=. */ add_action('wp_ajax_mxchat_onboarding_save_step', 'mxchat_onboarding_ajax_save_step'); function mxchat_onboarding_ajax_save_step() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission.', 'mxchat')), 403); } check_ajax_referer('mxchat_onboarding_nonce', 'nonce'); $which = isset($_POST['which']) ? sanitize_key($_POST['which']) : ''; // Behavior step branch (plan-a2e4d6) — saves system_prompt_instructions // into the SAME mxchat_options sub-key Settings uses, no parallel storage. if ($which === 'behavior') { $instructions = isset($_POST['system_prompt_instructions']) ? sanitize_textarea_field(wp_unslash($_POST['system_prompt_instructions'])) : ''; $opts = get_option('mxchat_options', array()); if (!is_array($opts)) $opts = array(); $opts['system_prompt_instructions'] = $instructions; update_option('mxchat_options', $opts); mxchat_onboarding_set_progress(array('behavior' => true)); wp_send_json_success(array( 'progress' => mxchat_onboarding_get_progress(), )); } $provider = isset($_POST['provider']) ? sanitize_key($_POST['provider']) : ''; $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : ''; $api_key = isset($_POST['api_key']) ? sanitize_text_field(wp_unslash($_POST['api_key'])) : ''; $catalog = mxchat_onboarding_provider_catalog(); if (!in_array($which, array('chat', 'embedding'), true)) { wp_send_json_error(array('message' => __('Invalid step.', 'mxchat')), 400); } if (!isset($catalog[$provider])) { wp_send_json_error(array('message' => __('Unknown provider.', 'mxchat')), 400); } $model_list = $which === 'chat' ? $catalog[$provider]['chat_models'] : $catalog[$provider]['embedding_models']; if (!isset($model_list[$model])) { wp_send_json_error(array('message' => __('Model not valid for this provider.', 'mxchat')), 400); } $opts = get_option('mxchat_options', array()); if (!is_array($opts)) $opts = array(); // Persist the key if a fresh one was sent. Empty string means "no change" // (the UI sends '' when the user is reusing an already-saved key). if ($api_key !== '') { $opts[$catalog[$provider]['key_option']] = $api_key; } if ($which === 'chat') { $opts['model'] = $model; } else { $opts['embedding_model'] = $model; } update_option('mxchat_options', $opts); $progress_key = $which === 'chat' ? 'chat_model' : 'embedding_model'; mxchat_onboarding_set_progress(array($progress_key => true)); wp_send_json_success(array( 'progress' => mxchat_onboarding_get_progress(), )); } /** * AJAX: mark a step done (used by Step 4 actions Skip/Continue, and Step 3 * Continue once KB is populated). No-op if the step name is unknown. */ add_action('wp_ajax_mxchat_onboarding_mark_step', 'mxchat_onboarding_ajax_mark_step'); function mxchat_onboarding_ajax_mark_step() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission.', 'mxchat')), 403); } check_ajax_referer('mxchat_onboarding_nonce', 'nonce'); $step = isset($_POST['step']) ? sanitize_key($_POST['step']) : ''; $valid = array('chat_model', 'behavior', 'embedding_model', 'knowledge_base', 'actions'); if (!in_array($step, $valid, true)) { wp_send_json_error(array('message' => __('Unknown step.', 'mxchat')), 400); } mxchat_onboarding_set_progress(array($step => true)); wp_send_json_success(array('progress' => mxchat_onboarding_get_progress())); } /** * AJAX: auto-graduate on Step 5 land. Sets dismissed + auto_graduated flags * (same flags f7c7d4 used) so the menu item disappears on next admin nav. */ add_action('wp_ajax_mxchat_onboarding_auto_graduate', 'mxchat_onboarding_ajax_auto_graduate'); function mxchat_onboarding_ajax_auto_graduate() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You do not have permission.', 'mxchat')), 403); } check_ajax_referer('mxchat_onboarding_nonce', 'nonce'); update_option('mxchat_onboarding_dismissed', 1); update_option('mxchat_onboarding_auto_graduated', 1); wp_send_json_success(array('graduated' => true)); } /** * Render the Onboarding page — wizard layout. Entry point wired from * class-mxchat-admin.php. */ function mxchat_render_onboarding_page() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to access this page.', 'mxchat')); } $opts = get_option('mxchat_options', array()); if (!is_array($opts)) $opts = array(); $progress = mxchat_onboarding_get_progress(); $catalog = mxchat_onboarding_provider_catalog(); // Compute saved-key state per provider so the JS can short-circuit the // API-key input to the "already saved" checkmark on initial render. $key_state = array(); foreach ($catalog as $slug => $entry) { $key_state[$slug] = mxchat_onboarding_provider_has_key($slug); } // Current selections (pre-populate the wizard if user already had values // in Settings before opening Onboarding). $current_chat_model = isset($opts['model']) ? (string) $opts['model'] : ''; $current_chat_provider = mxchat_onboarding_provider_from_model($current_chat_model); $current_embed_model = isset($opts['embedding_model']) ? (string) $opts['embedding_model'] : ''; $current_embed_provider = mxchat_onboarding_provider_from_embedding_model($current_embed_model); // Pre-credit logic — run BEFORE deciding the initial step so the landing // step reflects pre-existing Settings values. The wizard is six steps now // (plan-a2e4d6 inserted "AI Behavior" at position 2): // 1. Chat model → progress.chat_model // 2. AI Behavior → progress.behavior (NEW) // 3. Embedding → progress.embedding_model // 4. Knowledge base→ progress.knowledge_base // 5. Actions → progress.actions // 6. Congrats → no progress flag (all-five-complete view) // Pre-credit Step 1 (chat) if a chat model + key are already saved. if (!$progress['chat_model'] && $current_chat_model !== '' && $current_chat_provider !== '' && !empty($key_state[$current_chat_provider])) { mxchat_onboarding_set_progress(array('chat_model' => true)); $progress['chat_model'] = true; } // Pre-credit Step 2 (behavior) if system_prompt_instructions is non-empty // — the user already set this in Settings before opening Onboarding. $current_instructions = isset($opts['system_prompt_instructions']) ? (string) $opts['system_prompt_instructions'] : ''; if (!$progress['behavior'] && trim($current_instructions) !== '') { mxchat_onboarding_set_progress(array('behavior' => true)); $progress['behavior'] = true; } // Pre-credit Step 3 (embedding) if model + key already saved. if (!$progress['embedding_model'] && $current_embed_model !== '' && $current_embed_provider !== '' && !empty($key_state[$current_embed_provider])) { mxchat_onboarding_set_progress(array('embedding_model' => true)); $progress['embedding_model'] = true; } // Pre-credit Step 4 (KB) if rows already exist. if (!$progress['knowledge_base'] && mxchat_onboarding_kb_count() > 0) { mxchat_onboarding_set_progress(array('knowledge_base' => true)); $progress['knowledge_base'] = true; } // First-not-done step is where we land on load. With all five flags true, // we land on Step 6 (Congrats). if (!$progress['chat_model']) $initial_step = 1; elseif (!$progress['behavior']) $initial_step = 2; elseif (!$progress['embedding_model']) $initial_step = 3; elseif (!$progress['knowledge_base']) $initial_step = 4; elseif (!$progress['actions']) $initial_step = 5; else $initial_step = 6; $plugin_url = plugin_dir_url(dirname(__FILE__)); $dismiss_nonce = wp_create_nonce('mxchat_dismiss_onboarding'); $wizard_nonce = wp_create_nonce('mxchat_onboarding_nonce'); $current_slug = isset($_GET['page']) ? sanitize_key($_GET['page']) : 'mxchat-onboarding'; $nav_items = array( array('slug' => 'mxchat-onboarding', 'label' => __('Onboarding', 'mxchat'), 'svg' => ''), array('slug' => 'mxchat-settings', 'label' => __('Settings', 'mxchat'), 'svg' => ''), array('slug' => 'mxchat-prompts', 'label' => __('Knowledge', 'mxchat'), 'svg' => ''), array('slug' => 'mxchat-transcripts', 'label' => __('Transcripts', 'mxchat'), 'svg' => ''), array('slug' => 'mxchat-actions', 'label' => __('Actions', 'mxchat'), 'svg' => ''), array('slug' => 'mxchat-content', 'label' => __('Content', 'mxchat'), 'svg' => ''), array('slug' => 'mxchat-api-access', 'label' => __('API Access', 'mxchat'), 'svg' => ''), array('slug' => 'mxchat-activation', 'label' => __('Pro & Extensions','mxchat'), 'svg' => ''), ); // Build a JS-shaped catalog for the wizard's client-side step machine. $js_catalog = array(); foreach ($catalog as $slug => $entry) { $js_catalog[$slug] = array( 'label' => $entry['label'], 'chatModels' => $entry['chat_models'], 'embeddingModels' => $entry['embedding_models'], 'hasKey' => $key_state[$slug], 'requiresKeyToLoadModels' => !empty($entry['requires_key_to_load_models']), ); } // Step metadata for the clickable pill indicator (plan-a2e4d6). Each entry: // - id: progress flag this step gates (null for Congrats) // - label: short pill label // - title: full step heading (also exposed for the legacy "Step N of M" // text-fallback) $steps_meta = array( array('num' => 1, 'flag' => 'chat_model', 'label' => __('Chat model', 'mxchat'), 'title' => __('Choose your chat model', 'mxchat')), array('num' => 2, 'flag' => 'behavior', 'label' => __('Behavior', 'mxchat'), 'title' => __('Tell your chatbot how to behave', 'mxchat')), array('num' => 3, 'flag' => 'embedding_model', 'label' => __('Embedding', 'mxchat'), 'title' => __('Choose your embedding model', 'mxchat')), array('num' => 4, 'flag' => 'knowledge_base', 'label' => __('Knowledge', 'mxchat'), 'title' => __('Add to your knowledge base', 'mxchat')), array('num' => 5, 'flag' => 'actions', 'label' => __('Actions', 'mxchat'), 'title' => __('Try MxChat Actions', 'mxchat')), array('num' => 6, 'flag' => null, 'label' => __('Done', 'mxchat'), 'title' => __('You\'re set up', 'mxchat')), ); $wizard_config = array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => $wizard_nonce, 'catalog' => $js_catalog, 'initialStep' => (int) $initial_step, 'progress' => $progress, 'currentChatProvider' => $current_chat_provider, 'currentChatModel' => $current_chat_model, 'currentEmbedProvider'=> $current_embed_provider, 'currentEmbedModel' => $current_embed_model, 'currentInstructions' => $current_instructions, 'kbCount' => mxchat_onboarding_kb_count(), 'stepsMeta' => $steps_meta, 'urls' => array( 'kb' => admin_url('admin.php?page=mxchat-prompts'), 'actions' => admin_url('admin.php?page=mxchat-actions'), 'apiKeys' => admin_url('admin.php?page=mxchat-settings&tab=api-keys'), 'homepage' => home_url('/'), 'proAddons' => 'https://mxchat.ai/add-ons/', 'settingsTesting' => admin_url('admin.php?page=mxchat-settings#testing'), 'settingsDisplay' => admin_url('admin.php?page=mxchat-settings#chatbot-display'), ), 'strings' => array( 'keyAlreadySaved' => __('%s API key already saved.', 'mxchat'), 'embedKeyReused' => __('You\'ve already added your %s API key — using it for embeddings too.', 'mxchat'), 'replaceKey' => __('Replace key', 'mxchat'), 'saveKey' => __('Save key', 'mxchat'), 'saving' => __('Saving…', 'mxchat'), 'saved' => __('Saved', 'mxchat'), 'saveError' => __('Save failed. Try again or set the key in Settings → API Keys.', 'mxchat'), 'kbNone' => __('No knowledge base items yet.', 'mxchat'), /* translators: %d = number of KB items */ 'kbFoundOne' => __('Found 1 knowledge base item.', 'mxchat'), /* translators: %d = number of KB items */ 'kbFoundMany' => __('Found %d knowledge base items.', 'mxchat'), 'manageAllKeys' => __('You can also manage all your keys later in API Keys settings.', 'mxchat'), 'selectProvider' => __('— Select a provider —', 'mxchat'), 'selectModel' => __('— Select a model —', 'mxchat'), 'disabledHintOpenrouter' => __('OpenRouter requires setup in the main Settings page — finish onboarding, then configure it there.', 'mxchat'), 'disabledHintCustom' => __('Custom (OpenAI-compatible) providers need a Base URL and model configured in Settings → API Keys — finish onboarding, then set it up there.', 'mxchat'), 'shortcodeCopied' => __('Copied!', 'mxchat'), 'shortcodeCopy' => __('Copy', 'mxchat'), ), ); ?>

1 6