to your AddSynapse account? [Yes, connect] [Cancel]» screen. * Nothing is connected without «Yes, connect». Once per 10 minutes at most (no loop); otherwise nothing happens and the * usual screens show. */ public static function maybe_start_from_claude(): void { if ( wp_doing_ajax() || is_network_admin() || ! current_user_can( 'manage_options' ) ) { return; } $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_key( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET'; if ( 'GET' !== $method ) { return; } global $pagenow; $just = (int) get_transient( self::JUST_ACTIVATED ); if ( $just > 0 && 'plugins.php' === $pagenow ) { delete_transient( self::JUST_ACTIVATED ); } else { $just = 0; } $wizard = 'admin.php' === $pagenow && AddsynapseMenu::PARENT_SLUG === self::flag( 'page' ) && '' === self::flag( 'as_relay' ) && '' === self::flag( 'setup' ) && '' === self::flag( 'view' ) && '' === self::flag( 'welcome' ); if ( ( get_current_user_id() !== $just && ! $wizard ) || get_transient( self::AUTO_STARTED ) || self::is_set_up() || ! RelayKeys::transport_ok() ) { // Not «needs_setup()»: a wizard finished on an earlier install (the option outlives deactivation) must not stop // a new connection started in Claude (satest 24/10). Only a working connection does. return; } if ( ! PendingOnboarding::from_claude() ) { return; } $url = Api::claude_connect_url( home_url( '/' ), 'claude' ); if ( ! Api::link_allowed( $url ) ) { return; } set_transient( self::AUTO_STARTED, 1, PendingOnboarding::TTL ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- AddSynapse's own connect page (Api::link_allowed: https on an AddSynapse host only). wp_redirect( $url, 302 ); exit; } /** * Home address. * * @param array $args Extra query args. */ public static function url( array $args = array() ): string { return add_query_arg( array_merge( array( 'page' => AddsynapseMenu::PARENT_SLUG ), $args ), admin_url( 'admin.php' ) ); } /** * Address of one wizard path. * * @param string $view choose | mcp | key. */ public static function setup_url( string $view ): string { return self::url( array( 'setup' => $view ) ); } /** * A display-only query value (which view to show), matched against fixed lists by the callers. * * @param string $name Query arg. */ private static function flag( string $name ): string { return sanitize_key( (string) filter_input( INPUT_GET, $name, FILTER_UNSAFE_RAW ) ); } /** * Wizard state. * * @return array{done:bool,path:string} */ public static function state(): array { $raw = get_option( self::OPTION, array() ); $raw = is_array( $raw ) ? $raw : array(); return array( 'done' => ! empty( $raw['done'] ), 'path' => in_array( $raw['path'] ?? '', array( 'mcp', 'key' ), true ) ? (string) $raw['path'] : '', ); } /** * Whether at least one way works: the chat has a tested key, or AddSynapse holds an approved application password. * * @param array|null $connection SetupStatus::connection() (computed when null). */ public static function is_set_up( ?array $connection = null ): bool { if ( KeyStore::active() ) { return true; } $connection = $connection ?? SetupStatus::connection(); return ! empty( $connection['connected'] ); } /** * Whether the manager has not started yet: the wizard was never finished or skipped, and no way works. While true, * every entry point of the plugin (Plugins row, Home) opens the wizard. */ public static function needs_setup(): bool { return ! self::state()['done'] && ! self::is_set_up(); } /** * Which screen to show: a requested wizard path, the wizard (first visits, nothing set up), or Home. */ public static function view(): string { $setup = self::flag( 'setup' ); if ( in_array( $setup, self::VIEWS, true ) ) { return $setup; } if ( 'home' === self::flag( 'view' ) || ! self::needs_setup() ) { return 'home'; } return self::from_claude() ? 'mcp' : 'choose'; } /** * 1.3.5 (owner 24/10): the manager started in Claude (AddSynapse says a connection for this site address is pending), * so the first screen skips «Choose how you want to connect» and opens the Claude path with one button. Only on the * wizard's default screen (no `setup`/`view` asked for) of a site with nothing set up; «Other options» (setup=choose) * always shows both ways. Any failure of the question = the usual first screen. */ public static function from_claude(): bool { if ( '' !== self::flag( 'setup' ) || 'home' === self::flag( 'view' ) || ! current_user_can( Config::capability() ) || self::is_set_up() ) { return false; } return PendingOnboarding::from_claude(); } /** * «Finish» (and «Skip for now»): remembers that the manager went through the wizard, then shows Home. */ public function handle_finish(): void { if ( ! current_user_can( Config::capability() ) ) { wp_die( esc_html__( 'You are not allowed to access this page.', 'addsynapse-store-assistant' ), 403 ); } check_admin_referer( self::ACTION_FINISH ); $path = isset( $_POST['addsynapse_assistant_path'] ) ? sanitize_key( wp_unslash( $_POST['addsynapse_assistant_path'] ) ) : ''; update_option( self::OPTION, array( 'done' => true, 'path' => in_array( $path, array( 'mcp', 'key' ), true ) ? $path : self::state()['path'], ), false ); wp_safe_redirect( self::url( in_array( $path, array( 'mcp', 'key' ), true ) ? array( 'welcome' => $path ) : array() ) ); exit; } /** * «Disconnect AddSynapse» (Home): revokes every AddSynapse connection key and application password of this site at once * (RemoteAccess::revoke_all, as on deactivation). AddSynapse can no longer reach the site; the approval log stays. */ public static function handle_disconnect(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to access this page.', 'addsynapse-store-assistant' ), 403 ); } check_admin_referer( self::ACTION_DISCONNECT ); RemoteAccess::revoke_all(); wp_safe_redirect( self::url( array( 'welcome' => 'disconnected' ) ) ); exit; } /** * Old addresses (menu items of 1.3.0-dev and earlier) → their new place. Runs on admin_menu:1, before any output. */ public static function legacy_redirects(): void { $page = self::flag( 'page' ); if ( '' === $page || ! current_user_can( Config::capability() ) ) { return; } $target = ''; if ( AdminPage::SLUG === $page && 'settings' === self::flag( 'tab' ) ) { $target = SettingsPage::url(); } elseif ( ConnectPage::SLUG === $page ) { $target = 'log' === self::flag( 'tab' ) ? SettingsPage::url( array( 'view' => 'log' ) ) : self::setup_url( 'mcp' ); } elseif ( ProOffers::MENU_SLUG === $page ) { ( new ProOffers() )->redirect(); return; } if ( '' !== $target ) { wp_safe_redirect( $target ); exit; } } /** * Styles + script of Home / the wizard. */ public static function enqueue(): void { AdminPage::enqueue_admin_css(); $css = ADDSYNAPSE_ASSISTANT_PLUGIN_DIR . 'assets/home.css'; $js = ADDSYNAPSE_ASSISTANT_PLUGIN_DIR . 'assets/home.js'; wp_enqueue_style( self::HANDLE, ADDSYNAPSE_ASSISTANT_PLUGIN_URL . 'assets/home.css', array(), ADDSYNAPSE_ASSISTANT_VERSION . '.' . ( is_readable( $css ) ? (int) filemtime( $css ) : 0 ) ); wp_enqueue_script( self::HANDLE, ADDSYNAPSE_ASSISTANT_PLUGIN_URL . 'assets/home.js', array( 'wp-api-fetch' ), ADDSYNAPSE_ASSISTANT_VERSION . '.' . ( is_readable( $js ) ? (int) filemtime( $js ) : 0 ), true ); wp_localize_script( self::HANDLE, 'addsynapseHome', array( 'check' => '/' . RemoteAccess::REST_NAMESPACE . '/connect/check', 'status' => '/' . RemoteAccess::REST_NAMESPACE . '/connect/status', 'poll' => 'mcp' === self::view(), 'i18n' => array( 'checking' => __( 'Checking…', 'addsynapse-store-assistant' ), 'passed' => __( 'All checks passed.', 'addsynapse-store-assistant' ), 'failed' => __( 'Some checks need attention.', 'addsynapse-store-assistant' ), 'copied' => __( 'Copied', 'addsynapse-store-assistant' ), /* translators: %s: WordPress user name */ 'approved' => __( 'Approved by %s.', 'addsynapse-store-assistant' ), 'read' => __( 'Your AI read the store. You are all set.', 'addsynapse-store-assistant' ), 'checked' => __( 'AddSynapse checked the connection. You are all set.', 'addsynapse-store-assistant' ), 'done' => __( 'Done', 'addsynapse-store-assistant' ), 'error' => __( 'Something went wrong. Try again.', 'addsynapse-store-assistant' ), ), ) ); } /** * Screen body (called by our Overview renderer). */ public static function render(): void { $view = RelayClaim::has_confirm() ? 'mcp' : self::view(); echo '
'; // WordPress moves admin notices after the first heading; keep them above our hero. echo '

' . esc_html( 'AddSynapse Store Assistant' ) . '


'; if ( RelayClaim::has_confirm() ) { self::render_relay_confirm(); } elseif ( 'choose' === $view ) { self::render_choose(); } elseif ( 'mcp' === $view ) { self::render_mcp(); } elseif ( 'key' === $view ) { self::render_key(); } else { self::render_home(); } echo '
'; } /** * Small inline SVG icons (no external assets). Only fixed markup of this class goes through here. * * @param string $name Icon. */ private static function icon( string $name ): string { $paths = array( 'chat' => '', 'cloud' => '', 'check' => '', 'key' => '', 'back' => '', 'spark' => '', ); return ''; } /** * Allowed SVG markup for wp_kses(). * * @return array> */ private static function svg_tags(): array { $common = array( 'd' => true, 'cx' => true, 'cy' => true, 'r' => true, ); return array( 'svg' => array( 'class' => true, 'viewbox' => true, 'width' => true, 'height' => true, 'aria-hidden' => true, 'focusable' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, ), 'path' => $common, 'circle' => $common, ); } /** * Prints an icon. * * @param string $name Icon. */ private static function print_icon( string $name ): void { echo wp_kses( self::icon( $name ), self::svg_tags() ); } /** * A read-only text with a «Copy» button. * * @param string $value Text. * @param string $label Accessible label of the field. * @param string $button Button text. * @param string $mark Step the copy marks as done ('' = none). */ private static function copy_field( string $value, string $label, string $button, string $mark = '' ): void { $id = 'adsa-copy-' . substr( md5( $value ), 0, 8 ); echo '
'; echo ''; echo ''; echo ''; echo '
'; } /** * The progress bar of a path. * * @param array $steps Step id → [label, done]. */ private static function progress( array $steps ): void { $current = ''; foreach ( $steps as $id => $step ) { if ( ! $step[1] ) { $current = $id; break; } } echo '
    '; $n = 0; foreach ( $steps as $id => $step ) { ++$n; $class = 'adsa-progress__item' . ( $step[1] ? ' is-done' : '' ) . ( $id === $current ? ' is-current' : '' ); echo '
  1. '; echo '' . esc_html( (string) $n ) . ''; self::print_icon( 'check' ); echo '' . esc_html( $step[0] ) . ''; echo '' . esc_html( $step[1] ? __( '(done)', 'addsynapse-store-assistant' ) : '' ) . '
  2. '; } echo '
'; } /** * Opening markup of a step card. * * @param string $id Step id. * @param int $n Number. * @param string $title Title. * @param bool $done Done. */ private static function step_open( string $id, int $n, string $title, bool $done ): void { echo '
  • '; echo '

    ' . esc_html( $title ) . ' ' . esc_html__( 'Done', 'addsynapse-store-assistant' ) . '

    '; echo '
    '; } /** * Closing markup of a step card. */ private static function step_close(): void { echo '
  • '; } /** * «← Choose another way». */ private static function back_link(): void { if ( self::from_claude() ) { // Not a lock (wp.org guideline 5): both ways stay one click away. echo ''; self::print_icon( 'back' ); echo esc_html__( 'Other options', 'addsynapse-store-assistant' ) . ''; return; } $target = self::state()['done'] || self::is_set_up() ? self::url() : self::setup_url( 'choose' ); echo ''; self::print_icon( 'back' ); echo esc_html( self::state()['done'] || self::is_set_up() ? __( 'Back to Home', 'addsynapse-store-assistant' ) : __( 'Choose another way', 'addsynapse-store-assistant' ) ) . ''; } /** * The «Finish» form of a path. * * @param string $path mcp | key. * @param string $note Text next to the button. * @param bool $primary The screen's main button (false: a plain one, when another button is the main one). */ private static function finish( string $path, string $note, bool $primary = true ): void { echo '
    '; wp_nonce_field( self::ACTION_FINISH ); echo ''; echo ''; echo ''; echo '

    ' . esc_html( $note ) . '

    '; echo '
    '; } /** * Wizard, first screen: the hero and the two ways. */ private static function render_choose(): void { echo '
    '; echo '

    '; self::print_icon( 'spark' ); echo esc_html__( 'Welcome to Store Assistant', 'addsynapse-store-assistant' ) . '

    '; echo '

    ' . esc_html( self::is_site() ? __( 'Manage your site by talking to AI', 'addsynapse-store-assistant' ) : __( 'Manage your store by talking to AI', 'addsynapse-store-assistant' ) ) . '

    '; echo '

    ' . esc_html( self::is_site() ? __( 'Ask in plain words — «which pages have no SEO description?», «what is in my main menu?». Every change is a draft you approve, and you can undo it.', 'addsynapse-store-assistant' ) : __( 'Ask in plain words — «lower the Coffee prices by 5%», «which products have no image?». Every change is a draft you approve, and you can undo it.', 'addsynapse-store-assistant' ) ) . '

    '; echo '
      '; foreach ( array( __( 'Draft', 'addsynapse-store-assistant' ), __( 'Approve', 'addsynapse-store-assistant' ), __( 'Undo', 'addsynapse-store-assistant' ) ) as $i => $label ) { echo '
    1. ' . esc_html( (string) ( $i + 1 ) ) . '' . esc_html( $label ) . '
    2. '; } echo '
    '; echo '

    ' . esc_html__( 'Choose how you want to connect', 'addsynapse-store-assistant' ) . '

    '; echo '
    '; self::choice( 'mcp', 'cloud', __( 'Use it from Claude or ChatGPT', 'addsynapse-store-assistant' ), self::is_site() ? __( 'Talk to your site from Claude or ChatGPT.', 'addsynapse-store-assistant' ) : __( 'Talk to your store from Claude or ChatGPT.', 'addsynapse-store-assistant' ), array( __( 'No API key needed', 'addsynapse-store-assistant' ), __( 'Uses your Claude or ChatGPT subscription', 'addsynapse-store-assistant' ), /* translators: %s: number of actions per day, e.g. 100. */ sprintf( __( 'Free: %s actions a day', 'addsynapse-store-assistant' ), number_format_i18n( PlanInfo::FREE_PER_DAY ) ), __( 'Changes arrive as drafts: Approve, then Undo if needed', 'addsynapse-store-assistant' ), ), true ); self::choice( 'key', 'chat', __( 'Chat here in WordPress', 'addsynapse-store-assistant' ), __( 'A chat inside wp-admin, with your own AI key.', 'addsynapse-store-assistant' ), array( __( 'Works with an Anthropic or OpenAI API key', 'addsynapse-store-assistant' ), __( 'You pay your AI provider directly for what you use', 'addsynapse-store-assistant' ), __( 'The same drafts, Approve and Undo', 'addsynapse-store-assistant' ), ), false ); echo '
    '; echo '

    ' . esc_html__( 'You can use both ways, and switch at any time from Home.', 'addsynapse-store-assistant' ) . ' '; echo '' . esc_html__( 'Skip for now', 'addsynapse-store-assistant' ) . '

    '; } /** * One big choice card. * * @param string $view Path. * @param string $icon Icon. * @param string $title Title. * @param string $lead One line. * @param string[] $points Bullet points. * @param bool $recommended Recommended badge. */ private static function choice( string $view, string $icon, string $title, string $lead, array $points, bool $recommended ): void { $id = 'adsa-choice-' . $view; echo '
    '; if ( $recommended ) { echo '' . esc_html__( 'Recommended', 'addsynapse-store-assistant' ) . ''; } echo ''; self::print_icon( $icon ); echo ''; echo '

    ' . esc_html( $title ) . '

    '; echo '

    ' . esc_html( $lead ) . '

      '; foreach ( $points as $point ) { echo '
    • '; self::print_icon( 'check' ); echo esc_html( $point ) . '
    • '; } echo '
    '; echo '' . esc_html__( 'Continue', 'addsynapse-store-assistant' ) . ''; echo '
    '; } /** * A plain WordPress site (no WooCommerce): «site» wording instead of «store». */ private static function is_site(): bool { return 'site' === Environment::site_type(); } /** * The «Connect my site to AddSynapse at …» prompt (names AddSynapse so the AI picks our connect tool). */ public static function connect_prompt(): string { /* translators: %s: site address */ return sprintf( __( 'Connect my site to AddSynapse at %s', 'addsynapse-store-assistant' ), untrailingslashit( home_url() ) ); } /** * Path A: Claude / ChatGPT through AddSynapse. */ private static function render_mcp(): void { $status = SetupStatus::connection(); // Came from Claude and just pressed «Yes, connect» (AddSynapse sent the browser back): a clean «Done» screen, not // the three steps (owner 24/10). The steps stay one click away. if ( ( ! empty( $status['ready'] ) || ! empty( $status['connected'] ) ) && 'connected' === self::flag( 'as_relay' ) && PendingOnboarding::cached_from_claude() ) { self::render_arrival_done(); return; } // Connected for sure (AddSynapse confirmed the password, or used it): every step is done. $ready = ! empty( $status['ready'] ); // 1.3.4: AddSynapse connects with this plugin's own key, so Application Passwords being off (Wordfence's default) no // longer blocks anything; only HTTPS is required (RelayKeys::transport_ok). // 1.3.5 (owner 24/10): step 2 is ONE button that connects this site to the AddSynapse account (the account usually // already uses Claude); adding AddSynapse to an AI app moves to step 3, shown once the site is connected. $connected = $ready || (bool) $status['connected']; // Came from Claude and not connected yet: «Connect to Claude» / «Connect to ChatGPT» are the only primary buttons. $arrival = self::from_claude() && ! $connected; $steps = array( // A connected site has passed what the check checks (AddSynapse reached it): step 1 is done unless a real // check ran and failed (owner 24/10: never «not done» next to two green steps). 'check' => array( __( 'Check', 'addsynapse-store-assistant' ), $ready || (bool) $status['checked'] || ( $connected && empty( $status['check_failed'] ) ) ), 'connect' => array( __( 'Connect', 'addsynapse-store-assistant' ), $connected ), ); // 1.3.5 (owner 24/10): two steps — the check (runs by itself) and Connect — then «Finish». How to use it from // Claude or ChatGPT is on Home («You're all set!»). self::back_link(); echo '
    '; echo '

    ' . esc_html( self::is_site() ? __( 'Use your site from Claude or ChatGPT', 'addsynapse-store-assistant' ) : __( 'Use your store from Claude or ChatGPT', 'addsynapse-store-assistant' ) ) . '

    '; if ( $arrival ) { echo '

    ' . esc_html__( 'You came from Claude — one click to finish.', 'addsynapse-store-assistant' ) . ' ' . esc_html__( 'Press «Connect to Claude» below.', 'addsynapse-store-assistant' ) . '

    '; } else { echo '

    ' . esc_html__( 'Two short steps. Each step turns green on its own.', 'addsynapse-store-assistant' ) . '

    '; } echo '
    '; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only: a success line when AddSynapse sent the browser back. if ( $connected && isset( $_GET['as_relay'] ) && 'connected' === sanitize_key( wp_unslash( $_GET['as_relay'] ) ) ) { // Back from AddSynapse right after «Connect» (1.3.3, hub protocol 3). if ( PendingOnboarding::cached_from_claude() ) { // Came from Claude: the AddSynapse page in the other tab moves on by itself. echo '

    ' . esc_html__( 'Done — go back to Claude.', 'addsynapse-store-assistant' ) . ' ' . esc_html__( 'This site is now connected to AddSynapse. Ask Claude something about your site.', 'addsynapse-store-assistant' ) . '

    '; } else { echo '

    ' . esc_html__( 'Connected ✓', 'addsynapse-store-assistant' ) . ' ' . esc_html__( 'This site is now connected to AddSynapse. Go back to your AI chat and ask it something about your site.', 'addsynapse-store-assistant' ) . '

    '; } } if ( ! $ready && ! RelayKeys::transport_ok() ) { echo '

    ' . esc_html__( 'Before you connect: this site does not use HTTPS', 'addsynapse-store-assistant' ) . '

    '; echo '

    ' . esc_html__( 'AddSynapse connects only to sites that use HTTPS. Switch the site address to https:// (Settings → General), then check again.', 'addsynapse-store-assistant' ) . '

    '; echo '

    ' . esc_html__( 'Open General Settings', 'addsynapse-store-assistant' ) . ' ' . esc_html__( 'Check again', 'addsynapse-store-assistant' ) . '

    '; } self::progress( $steps ); echo '
      '; self::step_open( 'check', 1, __( 'Check the connection', 'addsynapse-store-assistant' ), $steps['check'][1] ); echo '

      ' . esc_html__( 'Checks HTTPS, the REST API and the sign-in header of this site. Runs on your server only.', 'addsynapse-store-assistant' ) . '

      '; // Runs by itself when the page opens (home.js); «Re-check» runs it again. echo '
      '; echo '

      '; self::step_close(); self::step_open( 'connect', 2, __( 'Connect this site to AddSynapse', 'addsynapse-store-assistant' ), $steps['connect'][1] ); if ( $connected ) { // Already connected (owner 21/10: a second «Connect my site» made a second password) — no connect button here. // 1.3.5: also while AddSynapse has not used the new key yet (pending): a second Connect would make a second key. echo '

      ' . esc_html__( 'Connected ✓', 'addsynapse-store-assistant' ) . '

      '; } else { self::render_connect_button(); $waiting = $status['connected'] /* translators: %s: user login */ ? sprintf( __( 'Approved by %s.', 'addsynapse-store-assistant' ), (string) $status['detail']['user'] ) : __( 'Waiting for your approval…', 'addsynapse-store-assistant' ); echo '

      ' . esc_html( $waiting ) . '

      '; // The older way, from the chat (the AI answers with the same confirm link) — kept for whoever starts there. echo '
      ' . esc_html__( 'Prefer to connect from your AI chat?', 'addsynapse-store-assistant' ) . ''; echo '

      ' . esc_html__( 'Send this to your AI. It answers with a link to this plugin\'s page on your site — confirm the connection there.', 'addsynapse-store-assistant' ) . '

      '; self::copy_field( self::connect_prompt(), __( 'Prompt to send', 'addsynapse-store-assistant' ), __( 'Copy prompt', 'addsynapse-store-assistant' ) ); echo '
      '; } self::step_close(); echo '
    '; // «Finish» is the main action once connected; before that «Connect to Claude» / «Connect to ChatGPT» are. self::finish( 'mcp', $connected ? __( 'Next: Home shows how to use it from Claude or ChatGPT.', 'addsynapse-store-assistant' ) : __( 'You can finish now and come back to these steps later from Home.', 'addsynapse-store-assistant' ), $connected ); echo '
    ' . esc_html__( 'More details: what AddSynapse can do on this site', 'addsynapse-store-assistant' ) . '
      '; echo '
    • ' . esc_html__( 'AddSynapse (addsynapse.com) is an external service. It connects only after you confirm it on this plugin\'s page, with a key this plugin creates for it.', 'addsynapse-store-assistant' ) . '
    • '; echo '
    • ' . esc_html__( 'That key opens only this plugin\'s store tools: read the store, products, categories and sales totals (no customer details), and write changes you approved. It cannot reach users, plugins, settings, other REST routes or XML-RPC.', 'addsynapse-store-assistant' ) . '
    • '; echo '
    • ' . esc_html__( 'Changes are prepared as drafts on AddSynapse. They are written here only after you press Approve, are listed in the Approval log, and can be undone.', 'addsynapse-store-assistant' ) . '
    • '; echo '
    • ' . esc_html__( 'Disconnect at any time: AddSynapse → Home → «Disconnect AddSynapse».', 'addsynapse-store-assistant' ) . '
    • '; echo '
    • ' . esc_html__( 'Deactivating or deleting this plugin also revokes the AddSynapse key (and any password WordPress issued to AddSynapse before), for all users of this site.', 'addsynapse-store-assistant' ) . '
    • '; echo '
    '; } /** * «Done — go back to Claude» (from=claude, right after «Yes, connect»). Nothing to press here: the AddSynapse page in * the other tab moves on by itself. A prompt to try, and links to Home and to the setup steps. */ private static function render_arrival_done(): void { self::render_allset( __( 'Done — go back to Claude.', 'addsynapse-store-assistant' ), __( 'This site is now connected to AddSynapse. Ask Claude something about your site.', 'addsynapse-store-assistant' ), false ); echo ''; } /** * The first prompt to paste into Claude or ChatGPT: it names the app (1.3.9 — ChatGPT uses an app only when the * prompt names it) and asks for something visible from this site. In the * admin's language; Greek is built in (I18n\FirstPrompt) until translate.wordpress.org has it (the text is sent to * the AI, so a Greek admin should get it in Greek). */ public static function first_prompt(): string { $site = self::is_site(); $prompt = $site ? __( 'Use Site Manager for WordPress to show me 3 pages from my site.', 'addsynapse-store-assistant' ) : __( 'Use Store Manager for WooCommerce to show me 3 products from my store.', 'addsynapse-store-assistant' ); $source = $site ? 'Use Site Manager for WordPress to show me 3 pages from my site.' : 'Use Store Manager for WooCommerce to show me 3 products from my store.'; if ( $prompt === $source ) { $built_in = \Addsynapse\StoreAssistant\I18n\FirstPrompt::built_in( (string) get_user_locale(), $site ); if ( '' !== $built_in ) { return $built_in; } } return $prompt; } /** * Two more example requests for Claude or ChatGPT (store or plain site), under the first prompt. * * @return string[] */ private static function try_prompts(): array { return self::is_site() ? array( __( 'Which pages have no SEO description?', 'addsynapse-store-assistant' ), __( 'What is in my main menu?', 'addsynapse-store-assistant' ), ) : array( __( 'Which products have no image?', 'addsynapse-store-assistant' ), __( 'What were my best sellers last month?', 'addsynapse-store-assistant' ), ); } /** * «You're all set!» — the first prompt to copy (primary), open Claude or ChatGPT, two more requests to copy, and (collapsed) how to add AddSynapse * to an AI app. Ordinary links the browser opens; nothing is sent by the server. * * @param string $title Heading. * @param string $lead One line under it ('' = none). * @param bool $dismiss Show «Hide» (Home only). */ private static function render_allset( string $title, string $lead, bool $dismiss ): void { echo '
    '; echo '

    ' . esc_html( $title ) . '

    '; if ( '' !== $lead ) { echo '

    ' . esc_html( $lead ) . '

    '; } // 1.3.6: ONE prompt first, large, with its own Copy button; then Open Claude; then two smaller examples. $first = self::first_prompt(); $id = 'adsa-copy-' . substr( md5( $first ), 0, 8 ); echo '
    '; echo '

    '; echo ''; echo '

    '; echo '' . esc_html__( 'Open Claude ↗', 'addsynapse-store-assistant' ) . ' ' . esc_html__( '(opens in a new tab)', 'addsynapse-store-assistant' ) . ' '; echo '' . esc_html__( 'Open ChatGPT ↗', 'addsynapse-store-assistant' ) . ' ' . esc_html__( '(opens in a new tab)', 'addsynapse-store-assistant' ) . '

    '; echo '
    '; echo '

    ' . esc_html__( 'More to try:', 'addsynapse-store-assistant' ) . '

    '; echo '
      '; foreach ( self::try_prompts() as $prompt ) { echo '
    • '; self::copy_field( $prompt, __( 'Request to send', 'addsynapse-store-assistant' ), __( 'Copy', 'addsynapse-store-assistant' ) ); echo '
    • '; } echo '
    '; echo '
    ' . esc_html__( 'AddSynapse not in your Claude or ChatGPT yet?', 'addsynapse-store-assistant' ) . ''; self::render_add_ways(); echo '
    '; if ( $dismiss ) { echo '
    '; wp_nonce_field( self::ACTION_ALLSET_HIDE ); echo ''; echo '
    '; } echo '
    '; } /** * Home: the «You're all set!» panel once connected — prominent for the first days (ALLSET_DAYS) or until hidden, * then collapsed. * * @param array $status SetupStatus::connection(). */ private static function render_home_allset( array $status ): void { if ( empty( $status['connected'] ) ) { return; } $state = get_option( self::ALLSET_OPTION, array() ); $state = is_array( $state ) ? $state : array(); if ( empty( $state['since'] ) ) { $state = array( 'since' => time(), 'hidden' => false, ); update_option( self::ALLSET_OPTION, $state, false ); } $fresh = empty( $state['hidden'] ) && ( time() - (int) $state['since'] ) < self::ALLSET_DAYS * DAY_IN_SECONDS; if ( $fresh ) { self::render_allset( __( 'You\'re all set!', 'addsynapse-store-assistant' ), '', true ); return; } echo '
    ' . esc_html__( 'Try it in Claude or ChatGPT', 'addsynapse-store-assistant' ) . ''; self::render_allset( __( 'You\'re all set!', 'addsynapse-store-assistant' ), '', false ); echo '
    '; } /** * «Hide» of the «You're all set!» panel (it stays, collapsed). */ public static function handle_allset_hide(): void { if ( ! current_user_can( Config::capability() ) ) { wp_die( esc_html__( 'You are not allowed to access this page.', 'addsynapse-store-assistant' ), 403 ); } check_admin_referer( self::ACTION_ALLSET_HIDE ); $state = get_option( self::ALLSET_OPTION, array() ); $state = is_array( $state ) ? $state : array(); $state['since'] = (int) ( $state['since'] ?? time() ); $state['hidden'] = true; update_option( self::ALLSET_OPTION, $state, false ); wp_safe_redirect( self::url( array( 'view' => 'home' ) ) ); exit; } /** * The «confirm the account» screen (back from WordPress' «Authorize Application» screen, or AddSynapse's start link): the wizard's head and progress (step 1 done, step 2 * current) with the «confirm the account» card of RelayClaim. */ private static function render_relay_confirm(): void { if ( PendingOnboarding::cached_from_claude() ) { // Keep the answer for the «Done» screen after «Yes, connect» (the confirm may take a while). PendingOnboarding::keep(); } if ( PendingOnboarding::cached_from_claude() && ! self::is_set_up() ) { // Not a lock (wp.org guideline 5): Cancel and both ways stay one click away. echo ''; self::print_icon( 'back' ); echo esc_html__( 'Other options', 'addsynapse-store-assistant' ) . ''; } echo '
    '; echo '

    ' . esc_html( self::is_site() ? __( 'Use your site from Claude or ChatGPT', 'addsynapse-store-assistant' ) : __( 'Use your store from Claude or ChatGPT', 'addsynapse-store-assistant' ) ) . '

    '; if ( PendingOnboarding::cached_from_claude() ) { echo '

    ' . esc_html__( 'You came from Claude — one click to finish.', 'addsynapse-store-assistant' ) . '

    '; } echo '
    '; self::progress( array( 'check' => array( __( 'Check', 'addsynapse-store-assistant' ), true ), 'connect' => array( __( 'Connect', 'addsynapse-store-assistant' ), false ), ) ); echo '
      '; RelayClaim::render_confirm_card(); echo '
    '; } /** * The line shown before the manager meets WordPress' own «Authorize Application» screen (owner 21/10: «why is this * needed?»). */ public static function authorize_note(): void { echo '

    ' . esc_html__( 'Next you\'ll see this plugin\'s page with the AddSynapse account — click «Yes, connect». It gives AddSynapse a key that works only for these tools; you can disconnect any time.', 'addsynapse-store-assistant' ) . '

    '; } /** * Step 2: the one button — AddSynapse's connect page for this site (sign in or create a free account, approve the * access, then confirm it on this plugin's page). An ordinary link the administrator's browser opens; nothing is sent * by the server. */ private static function render_connect_button(): void { echo '

    ' . esc_html( self::is_site() ? __( 'Links this site to your AddSynapse account, so Claude or ChatGPT can work with it.', 'addsynapse-store-assistant' ) : __( 'Links this store to your AddSynapse account, so Claude or ChatGPT can work with it.', 'addsynapse-store-assistant' ) ) . '

    '; // 1.3.9 (owner 12/11): one button per AI app, so AddSynapse sends the manager back to the right one. printf( '

    %s %s

    ', esc_url( \Addsynapse\StoreAssistant\Account\Api::claude_connect_url( home_url( '/' ), 'claude' ) ), esc_html__( 'Connect to Claude', 'addsynapse-store-assistant' ), esc_url( \Addsynapse\StoreAssistant\Account\Api::claude_connect_url( home_url( '/' ), 'chatgpt' ) ), esc_html__( 'Connect to ChatGPT', 'addsynapse-store-assistant' ) ); echo '

    ' . esc_html__( 'Free AddSynapse account — you sign in or create one.', 'addsynapse-store-assistant' ) . '

    '; self::authorize_note(); } /** * Step 3 (collapsed): how to add AddSynapse to an AI app — «Store Manager for WooCommerce» in Claude's connector * directory or in ChatGPT's apps (one click each, 1.3.9), or by address for other AI apps. */ private static function render_add_ways(): void { echo '

    ' . esc_html__( 'Once per AI account — the same app serves all your sites. Choose your AI app:', 'addsynapse-store-assistant' ) . '

    '; echo '
    '; echo ''; echo ''; echo '
    '; echo '
    ' . esc_html__( 'Cursor and other AI apps', 'addsynapse-store-assistant' ) . ''; echo '

    ' . esc_html__( 'Add a remote MCP server (Streamable HTTP) with this address. When the app asks you to sign in, sign in to AddSynapse.', 'addsynapse-store-assistant' ) . '

    '; self::copy_field( ConnectPage::mcp_url(), __( 'MCP server address:', 'addsynapse-store-assistant' ), __( 'Copy', 'addsynapse-store-assistant' ) ); echo '
    '; } /** * Whether the chat was used with the own key (any AI call in the last 30 days). */ private static function chat_used(): bool { $summary = KeyUsage::summary(); return (int) ( $summary['days30']['calls'] ?? 0 ) > 0; } /** * Path B: the chat here with the manager's own AI key. */ private static function render_key(): void { $settings = KeyStore::settings(); $active = KeyStore::active(); $notice = self::flag( 'addsynapse_assistant_notice' ); $steps = array( 'provider' => array( __( 'Provider', 'addsynapse-store-assistant' ), '' !== $settings['provider'] ), 'key' => array( __( 'API key', 'addsynapse-store-assistant' ), $active ), 'try' => array( __( 'Try the chat', 'addsynapse-store-assistant' ), $active && self::chat_used() ), ); self::back_link(); echo '
    '; echo '

    ' . esc_html__( 'Chat here in WordPress', 'addsynapse-store-assistant' ) . '

    '; echo '

    ' . esc_html__( 'Three steps. Your key stays on this server; you pay your AI provider for what you use.', 'addsynapse-store-assistant' ) . '

    '; echo '
    '; self::progress( $steps ); if ( ! OwnKeySection::can_manage() ) { echo '

    ' . esc_html__( 'Only an administrator can add the AI key. Ask an administrator of this site to open this page.', 'addsynapse-store-assistant' ) . '

    '; } $current = '' !== $settings['provider'] ? $settings['provider'] : Models::PROVIDERS[0]; $catalog = OwnKeySection::model_catalog(); $selected = '' !== $settings['model'] && Models::matches( $current, $settings['model'] ) ? $settings['model'] : Models::default_model( $current ); $links = array( 'anthropic' => 'https://console.anthropic.com/settings/keys', 'openai' => 'https://platform.openai.com/api-keys', ); echo '
    '; wp_nonce_field( OwnKeySection::ACTION_SAVE ); echo ''; echo ''; echo '
      '; self::step_open( 'provider', 1, __( 'Choose your AI provider', 'addsynapse-store-assistant' ), $steps['provider'][1] ); echo '
      ' . esc_html__( 'AI provider', 'addsynapse-store-assistant' ) . ''; $blurbs = array( 'anthropic' => __( 'Claude models. Recommended for this assistant.', 'addsynapse-store-assistant' ), 'openai' => __( 'GPT models.', 'addsynapse-store-assistant' ), ); foreach ( Models::PROVIDERS as $provider ) { echo ''; } echo '
      '; self::step_close(); self::step_open( 'key', 2, __( 'Add your API key', 'addsynapse-store-assistant' ), $steps['key'][1] ); OwnKeySection::render_notice( $notice ); if ( $active ) { /* translators: 1: provider name, 2: model id. */ echo '

      ' . esc_html( sprintf( __( 'Active: %1$s, model %2$s.', 'addsynapse-store-assistant' ), OwnKeySection::label( $settings['provider'] ), $settings['model'] ) ) . '

      '; } if ( OwnKeySection::can_manage() ) { echo '

      '; if ( '' !== $settings['provider'] && KeyStore::from_constant( $settings['provider'] ) ) { /* translators: %s: PHP constant name. */ echo '' . esc_html( sprintf( __( 'Set in wp-config.php (%s).', 'addsynapse-store-assistant' ), KeyStore::CONSTANTS[ $settings['provider'] ] ) ) . ''; } else { $hint = KeyStore::hint(); /* translators: %s: last characters of the saved key. */ $placeholder = '' !== $hint ? sprintf( __( 'Saved (%s) — leave empty to keep it', 'addsynapse-store-assistant' ), $hint ) : ''; echo ''; } echo '

      '; echo '

      ' . esc_html__( 'Get a key:', 'addsynapse-store-assistant' ) . ' '; foreach ( $links as $provider => $link ) { echo '' . esc_html( OwnKeySection::label( $provider ) ) . ' ' . esc_html__( '(opens in a new tab)', 'addsynapse-store-assistant' ) . ''; } echo '

      '; echo '

      '; echo '
      ' . esc_html__( 'More details', 'addsynapse-store-assistant' ) . ''; echo '

      ' . esc_html__( 'When you chat, your message, the conversation, a summary of the site structure and the results of the tools the assistant uses for your question (for example product names, prices, stock and sales figures — never customer names, emails or phone numbers) are sent from this server to the AI provider you choose, under that provider\'s terms and privacy policy. An image you attach in the chat is sent with that message (a reduced copy; you can turn this off below: «The AI sees the images you upload»). Nothing is sent before you press «Save and test», and nothing goes to AddSynapse.', 'addsynapse-store-assistant' ) . '

      '; echo '

      ' . esc_html__( 'Stored encrypted in the database (not autoloaded) and used only on this server; it is never shown again or sent to the browser. For extra safety define it in wp-config.php instead: ADDSYNAPSE_ASSISTANT_ANTHROPIC_KEY or ADDSYNAPSE_ASSISTANT_OPENAI_KEY.', 'addsynapse-store-assistant' ) . '

      '; echo '

      ' . esc_html__( 'Anthropic:', 'addsynapse-store-assistant' ) . ' ' . esc_html__( 'Terms', 'addsynapse-store-assistant' ) . ' · ' . esc_html__( 'Privacy', 'addsynapse-store-assistant' ) . ' — '; echo esc_html__( 'OpenAI:', 'addsynapse-store-assistant' ) . ' ' . esc_html__( 'Terms', 'addsynapse-store-assistant' ) . ' · ' . esc_html__( 'Privacy', 'addsynapse-store-assistant' ) . '

      '; echo '

      '; echo '
      '; echo '

      '; }//end if self::step_close(); self::step_open( 'try', 3, __( 'Try the chat', 'addsynapse-store-assistant' ), $steps['try'][1] ); echo '

      ' . esc_html__( 'Open the chat with a first question ready to send. It only reads the store.', 'addsynapse-store-assistant' ) . '

      '; echo '

      ' . esc_html( AdminPage::sample_prompt() ) . '

      '; if ( $active ) { echo '

      ' . esc_html__( 'Open the chat', 'addsynapse-store-assistant' ) . '

      '; } else { echo '

      ' . esc_html__( 'Available after the key test passes.', 'addsynapse-store-assistant' ) . '

      '; } self::step_close(); echo '
    '; self::finish( 'key', __( 'You can change the key, the model and the other options later in Settings.', 'addsynapse-store-assistant' ) ); } /** * Home: the status of both ways, quick actions, optional cards. */ private static function render_home(): void { $status = SetupStatus::connection(); $settings = KeyStore::settings(); $active = KeyStore::active(); $welcome = self::flag( 'welcome' ); echo '

    AddSynapse

    Store Assistant

    '; if ( ProOffers::shown() ) { echo '' . esc_html__( 'Upgrade to Pro', 'addsynapse-store-assistant' ) . ' ' . esc_html__( '(opens in a new tab)', 'addsynapse-store-assistant' ) . ''; } echo '
    '; if ( 'disconnected' === $welcome ) { echo '
    '; self::print_icon( 'check' ); echo '
    ' . esc_html__( 'Disconnected.', 'addsynapse-store-assistant' ) . ' '; echo esc_html__( 'AddSynapse can no longer reach this site. Connect again any time from your AI chat.', 'addsynapse-store-assistant' ) . '
    '; } elseif ( 'connected' === $welcome ) { // RelayClaim: the approval came back twice (refresh/back) and AddSynapse had already taken the first one. echo '
    '; self::print_icon( 'check' ); echo '
    ' . esc_html__( 'Already connected ✓', 'addsynapse-store-assistant' ) . ' '; echo esc_html__( 'This site is connected to AddSynapse. Go back to Claude or ChatGPT and continue there.', 'addsynapse-store-assistant' ) . '
    '; } elseif ( 'key' === $welcome || ( 'mcp' === $welcome && empty( $status['connected'] ) ) ) { echo '
    '; self::print_icon( 'check' ); echo '
    ' . esc_html__( 'You are all set.', 'addsynapse-store-assistant' ) . ' '; echo esc_html( 'mcp' === $welcome ? __( 'Ask Claude or ChatGPT about your store. Changes come back as drafts you approve.', 'addsynapse-store-assistant' ) : __( 'Ask the assistant in the Chat. Changes come back as drafts you approve.', 'addsynapse-store-assistant' ) ) . '
    '; } self::render_home_allset( $status ); Announcements::render( 'home' ); ReviewRequest::render( 'home' ); echo '
    '; // Way A. echo '
    '; self::print_icon( 'cloud' ); echo '

    ' . esc_html__( 'Claude or ChatGPT', 'addsynapse-store-assistant' ) . '

    '; self::pill( (bool) $status['connected'], $status['connected'] ? __( 'Connected', 'addsynapse-store-assistant' ) : __( 'Not set up', 'addsynapse-store-assistant' ) ); echo '
    '; if ( $status['connected'] ) { /* translators: %s: user login */ echo '

    ' . esc_html( sprintf( __( 'Approved by %s.', 'addsynapse-store-assistant' ), (string) $status['detail']['user'] ) ) . ' '; echo esc_html( null !== $status['detail']['last_used'] /* translators: %s: time difference, e.g. «5 mins» */ ? sprintf( __( 'Last used %s ago.', 'addsynapse-store-assistant' ), human_time_diff( (int) $status['detail']['last_used'] ) ) : __( 'Not used yet.', 'addsynapse-store-assistant' ) ) . '

    '; // 1.3.2: the plan and today's count (as AddSynapse last reported it) — and no second approve pushed here. echo '

    ' . esc_html( PlanInfo::line() ) . ' ' . esc_html__( 'Change plan', 'addsynapse-store-assistant' ) . ' ' . esc_html__( '(opens in a new tab)', 'addsynapse-store-assistant' ) . '

    '; echo '

    ' . esc_html( self::is_site() ? __( 'Nothing else to do here: ask Claude or ChatGPT about your site.', 'addsynapse-store-assistant' ) : __( 'Nothing else to do here: ask Claude or ChatGPT about your store.', 'addsynapse-store-assistant' ) ) . '

    '; echo '

    ' . esc_html__( 'Approval log', 'addsynapse-store-assistant' ) . '

    '; echo '
    ' . esc_html__( 'Connect another site or AI account', 'addsynapse-store-assistant' ) . ''; echo '

    ' . esc_html__( 'Only for a different AI account or app. In that chat, send:', 'addsynapse-store-assistant' ) . '

    '; self::copy_field( self::connect_prompt(), __( 'Prompt to send', 'addsynapse-store-assistant' ), __( 'Copy prompt', 'addsynapse-store-assistant' ) ); echo '

    ' . esc_html__( 'Setup steps', 'addsynapse-store-assistant' ) . '

    '; echo '
    '; if ( current_user_can( 'manage_options' ) ) { echo '
    '; wp_nonce_field( self::ACTION_DISCONNECT ); echo ''; echo ''; echo '
    '; } } else { echo '

    ' . esc_html( self::is_site() ? __( 'Talk to your site from Claude or ChatGPT. No API key needed.', 'addsynapse-store-assistant' ) : __( 'Talk to your store from Claude or ChatGPT. No API key needed.', 'addsynapse-store-assistant' ) ) . '

    '; echo '

    ' . esc_html( PlanInfo::line() ) . '

    '; echo '

    ' . esc_html( $active ? __( 'Use this way too', 'addsynapse-store-assistant' ) : __( 'Set up', 'addsynapse-store-assistant' ) ) . '

    '; }//end if echo '
    '; // Way B. echo '
    '; self::print_icon( 'chat' ); echo '

    ' . esc_html__( 'Chat here in WordPress', 'addsynapse-store-assistant' ) . '

    '; self::pill( $active, $active ? __( 'Ready', 'addsynapse-store-assistant' ) : __( 'Not set up', 'addsynapse-store-assistant' ) ); echo '
    '; if ( $active ) { /* translators: 1: provider name, 2: model id. */ echo '

    ' . esc_html( sprintf( __( 'Active: %1$s, model %2$s.', 'addsynapse-store-assistant' ), OwnKeySection::label( $settings['provider'] ), $settings['model'] ) ) . '

    '; echo '

    ' . esc_html__( 'Open the chat', 'addsynapse-store-assistant' ) . ' ' . esc_html__( 'Key and model', 'addsynapse-store-assistant' ) . '

    '; } else { echo '

    ' . esc_html__( 'A chat inside wp-admin, with your own Anthropic or OpenAI key.', 'addsynapse-store-assistant' ) . '

    '; echo '

    ' . esc_html( $status['connected'] ? __( 'Use this way too', 'addsynapse-store-assistant' ) : __( 'Set up', 'addsynapse-store-assistant' ) ) . '

    '; } echo '
    '; // Opt-in cards: only here (after setup), never on the first wizard screen. if ( self::is_set_up( $status ) || self::state()['done'] ) { echo '
    '; Usage::render( 'home' ); OptIn::render( 'home' ); echo '
    '; } echo ''; if ( ProOffers::shown() ) { echo ''; } $others = AddsynapseMenu::products(); unset( $others['ai-shop-assistant'] ); if ( $others ) { echo '

    ' . esc_html__( 'Other AddSynapse plugins', 'addsynapse-store-assistant' ) . '

    '; AddsynapseMenu::render_cards( $others ); } } /** * Status pill. * * @param bool $ok Green. * @param string $label Text. */ private static function pill( bool $ok, string $label ): void { echo '' . esc_html( $label ) . ''; } }