settings = $settings;
$this->syncRunner = $syncRunner;
$this->progress = $progress;
$this->agents = $agents;
$this->pluginPath = $pluginPath;
$this->pluginUrl = $pluginUrl;
$this->version = $version;
$this->onboarding = $onboarding;
}
public function register(): void
{
\add_action( 'admin_menu', [ $this, 'addMenuPage' ] );
\add_action( 'admin_post_' . self::SAVE_ACTION, [ $this, 'handleSubmit' ] );
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAssets' ] );
}
public function addMenuPage(): void
{
\add_menu_page( 'Agent001', 'Agent001', self::CAPABILITY, self::PAGE_SLUG, [ $this, 'render' ], 'dashicons-format-chat' );
}
public function enqueueAssets( string $hook ): void
{
if ( $hook !== 'toplevel_page_' . self::PAGE_SLUG ) {
return;
}
\wp_enqueue_style(
'agent001-sync',
$this->pluginUrl . 'assets/css/agent001-sync.css',
[],
$this->version
);
\wp_enqueue_script(
'agent001-sync',
$this->pluginUrl . 'assets/js/agent001-sync.js',
[],
$this->version,
true
);
\wp_enqueue_script(
'agent001-categories',
$this->pluginUrl . 'assets/js/agent001-categories.js',
[],
$this->version . '.' . (string) \filemtime( $this->pluginPath . 'assets/js/agent001-categories.js' ),
true
);
\wp_localize_script(
'agent001-sync',
'Agent001Sync',
[
'ajaxUrl' => \admin_url( 'admin-ajax.php' ),
'nonce' => \wp_create_nonce( SyncAjaxController::NONCE ),
'actions' => [
'start' => SyncAjaxController::START_ACTION,
'status' => SyncAjaxController::STATUS_ACTION,
],
'pollInterval' => 5000,
'i18n' => [
'syncNow' => \__( 'Sync now', 'agent001-support-assistant-for-woocommerce' ),
'syncing' => \__( 'Syncing…', 'agent001-support-assistant-for-woocommerce' ),
'idle' => \__( 'Idle', 'agent001-support-assistant-for-woocommerce' ),
'queued' => \__( 'Queued…', 'agent001-support-assistant-for-woocommerce' ),
'reading' => \__( 'Reading products…', 'agent001-support-assistant-for-woocommerce' ),
'posting' => \__( 'Sending to API…', 'agent001-support-assistant-for-woocommerce' ),
'indexing' => \__( 'Saving…', 'agent001-support-assistant-for-woocommerce' ),
'indexingHint' => \__( 'Large catalogs can take several minutes.', 'agent001-support-assistant-for-woocommerce' ),
'done' => \__( 'Sync completed.', 'agent001-support-assistant-for-woocommerce' ),
'error' => \__( 'Sync failed.', 'agent001-support-assistant-for-woocommerce' ),
'conflict' => \__( 'Another sync is already running.', 'agent001-support-assistant-for-woocommerce' ),
'network' => \__( 'Network error. Retrying…', 'agent001-support-assistant-for-woocommerce' ),
],
]
);
}
public function render(): void
{
if ( ! \current_user_can( self::CAPABILITY ) ) {
return;
}
$apiKeyLocked = $this->settings->apiKeyIsLocked();
$isConnected = $this->settings->hasApiKey();
$connectedSite = $this->settings->connectedSite();
$accountName = $this->settings->accountName();
$progress = $this->progress->current();
$selectedCatIds = $this->settings->syncCategories();
$availableCategories = $this->fetchAvailableCategories();
$availableAgents = [];
$selectedAgentIds = $this->settings->selectedAgentIds();
$agentsFetchFailed = false;
if ( $isConnected ) {
$cached = \get_transient( AgentSelectionController::AGENTS_TRANSIENT );
$cacheHasAgentStatus = is_array( $cached ) && isset( $cached['agents'] ) && is_array( $cached['agents'] )
&& array_filter( $cached['agents'], static fn( $agent ): bool => ! is_array( $agent ) || ! array_key_exists( 'active', $agent ) ) === [];
if ( is_array( $cached ) && isset( $cached['agents'] ) && ! empty( $cached['account_name'] ) && $cacheHasAgentStatus ) {
$availableAgents = $cached['agents'];
$cachedIds = is_array( $cached['selected_ids'] ?? null ) ? AgentId::normalizeList( $cached['selected_ids'] ) : [];
$selectedAgentIds = $cachedIds !== [] ? $cachedIds : $selectedAgentIds;
} else {
$agentsPayload = $this->agents->fetchAgents();
if ( $agentsPayload === null ) {
$agentsFetchFailed = true;
} else {
$availableAgents = $agentsPayload['agents'];
$selectedAgentIds = $agentsPayload['selected_ids'] !== []
? $agentsPayload['selected_ids']
: $selectedAgentIds;
}
}
}
$isOnboarding = $this->onboarding->isActive();
$onboardingPaused = $this->onboarding->isPaused();
$setupStep = $this->onboarding->step();
$onboarding = $this->onboarding;
$backendUrl = $this->settings->backendBaseUrl();
echo '
Agent001
';
echo '
';
echo '
' . \esc_html__( 'Chat not visible on your store? Clear your site cache.', 'agent001-support-assistant-for-woocommerce' ) . '
';
echo '
' . \esc_html__( 'Agent001 clears the cache of popular caching plugins automatically. If your store also uses a hosting cache or a CDN such as Cloudflare, clear it manually after finishing setup or changing the agent, then reload your store page.', 'agent001-support-assistant-for-woocommerce' ) . '
';
echo '
';
\settings_errors( 'agent001' );
echo '
';
}
public function handleSubmit(): void
{
if ( ! \current_user_can( self::CAPABILITY ) ) {
\wp_die( \esc_html__( 'Permission denied.', 'agent001-support-assistant-for-woocommerce' ), '', [ 'response' => 403 ] );
}
if ( isset( $_POST['agent001_toggle_agent'] ) ) {
$this->handleAgentToggle();
return;
}
$this->handleSave();
if ( \get_settings_errors( 'agent001' ) === [] ) {
\add_settings_error( 'agent001', 'saved', \__( 'Settings saved.', 'agent001-support-assistant-for-woocommerce' ), 'success' );
}
\set_transient( 'settings_errors', \get_settings_errors(), 30 );
\wp_safe_redirect( \add_query_arg( 'settings-updated', 'true', $this->onboarding->settingsUrl() ) );
exit;
}
private function handleAgentToggle(): void
{
\check_admin_referer( self::SAVE_ACTION, '_agent001_nonce' );
$rawAgentId = isset( $_POST['agent001_toggle_agent'] ) && is_scalar( $_POST['agent001_toggle_agent'] )
? \sanitize_text_field( \wp_unslash( (string) $_POST['agent001_toggle_agent'] ) )
: '';
$rawActive = isset( $_POST['agent001_toggle_active'] ) && is_scalar( $_POST['agent001_toggle_active'] )
? (string) \wp_unslash( $_POST['agent001_toggle_active'] )
: '';
$toggleAgentId = AgentId::normalize( $rawAgentId );
if ( $toggleAgentId === '' || ! in_array( $rawActive, [ '0', '1' ], true ) ) {
\add_settings_error( 'agent001', 'agent_toggle_error', \__( 'Invalid agent status update.', 'agent001-support-assistant-for-woocommerce' ) );
} else {
$error = $this->agents->setAgentActive( $toggleAgentId, $rawActive === '1' );
if ( $error !== null ) {
\add_settings_error( 'agent001', 'agent_toggle_error', $error );
} else {
\add_settings_error(
'agent001',
'agent_toggle_success',
$rawActive === '1'
? \__( 'Agent enabled.', 'agent001-support-assistant-for-woocommerce' )
: \__( 'Agent disabled.', 'agent001-support-assistant-for-woocommerce' ),
'success'
);
}
}
\set_transient( 'settings_errors', \get_settings_errors(), 30 );
\wp_safe_redirect( $this->onboarding->settingsUrl() );
exit;
}
/**
* @return array
*/
private function fetchAvailableCategories(): array
{
if ( ! \taxonomy_exists( 'product_cat' ) ) {
return [];
}
$terms = \get_terms(
[
'taxonomy' => 'product_cat',
'hide_empty' => false,
'orderby' => 'name',
]
);
if ( ! is_array( $terms ) ) {
return [];
}
$byParent = [];
foreach ( $terms as $term ) {
$byParent[ (int) $term->parent ][] = $term;
}
$ordered = [];
$walker = static function ( int $parent, int $depth ) use ( &$walker, &$byParent, &$ordered ): void {
if ( ! isset( $byParent[ $parent ] ) ) {
return;
}
foreach ( $byParent[ $parent ] as $term ) {
$ordered[] = [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'parent' => (int) $term->parent,
'count' => (int) $term->count,
'depth' => $depth,
];
$walker( (int) $term->term_id, $depth + 1 );
}
};
$walker( 0, 0 );
return $ordered;
}
public function handleSave(): void
{
if ( ! \current_user_can( self::CAPABILITY ) ) {
return;
}
\check_admin_referer( self::SAVE_ACTION, '_agent001_nonce' );
if ( $this->progress->isRunning() ) {
\add_settings_error( 'agent001', 'save_error', \__( 'Wait for the current synchronization to finish before changing the agent or products.', 'agent001-support-assistant-for-woocommerce' ) );
return;
}
$rawPost = isset( $_POST['agent001_settings'] ) && is_array( $_POST['agent001_settings'] )
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nested values are sanitized below per type.
? \map_deep( \wp_unslash( $_POST['agent001_settings'] ), 'sanitize_text_field' )
: [];
$raw = is_array( $rawPost ) ? $rawPost : [];
if ( $this->onboarding->isActive() && $this->onboarding->step() === 1 ) {
return;
}
if ( ! isset( $raw['agent_id'] ) || ! is_scalar( $raw['agent_id'] ) ) {
\add_settings_error( 'agent001', 'save_error', \__( 'Select an agent before saving.', 'agent001-support-assistant-for-woocommerce' ) );
return;
}
$rawCats = isset( $raw['sync_categories'] ) && is_array( $raw['sync_categories'] )
? $raw['sync_categories']
: [];
$rawAgentId = isset( $raw['agent_id'] ) ? (string) $raw['agent_id'] : '';
$selectedId = AgentId::normalize( $rawAgentId );
if ( $rawAgentId !== '' && $selectedId === '' ) {
\add_settings_error( 'agent001', 'save_error', \__( 'Invalid agent selection.', 'agent001-support-assistant-for-woocommerce' ) );
return;
}
$ids = [];
foreach ( $rawCats as $value ) {
if ( ! is_scalar( $value ) || ! ctype_digit( (string) $value ) ) {
\add_settings_error( 'agent001', 'save_error', \__( 'Invalid product category selection.', 'agent001-support-assistant-for-woocommerce' ) );
return;
}
$id = (int) $value;
if ( $id > 0 ) {
$ids[] = $id;
}
}
$newCategoryIds = array_values( array_unique( $ids ) );
sort( $newCategoryIds );
$availableCategoryIds = array_column( $this->fetchAvailableCategories(), 'id' );
if ( $availableCategoryIds !== [] && $newCategoryIds === [] ) {
\add_settings_error( 'agent001', 'save_error', \__( 'Select at least one product category.', 'agent001-support-assistant-for-woocommerce' ) );
return;
}
if ( array_diff( $newCategoryIds, $availableCategoryIds ) !== [] ) {
\add_settings_error( 'agent001', 'save_error', \__( 'Invalid product category selection.', 'agent001-support-assistant-for-woocommerce' ) );
return;
}
$previousCategoryIds = $this->settings->syncCategories();
sort( $previousCategoryIds );
$previousAgentIds = $this->settings->selectedAgentIds();
sort( $previousAgentIds );
$agentIds = [];
if ( $selectedId !== '' ) {
$agentIds[] = $selectedId;
}
$newAgentIds = array_values( array_unique( $agentIds ) );
sort( $newAgentIds );
if ( $this->onboarding->isActive() && $agentIds === [] ) {
\add_settings_error( 'agent001', 'save_error', \__( 'Select an agent to continue setup.', 'agent001-support-assistant-for-woocommerce' ) );
return;
}
if ( $this->settings->hasApiKey() ) {
$error = $this->agents->saveSelection( $agentIds );
if ( $error !== null ) {
\add_settings_error( 'agent001', 'save_error',
sprintf(
/* translators: %s: error message from remote */
\__( 'Agent selection: %s', 'agent001-support-assistant-for-woocommerce' ),
$error
)
);
return;
}
}
$this->settings->setSyncCategories( $newCategoryIds );
if ( $this->onboarding->isActive() && $this->settings->selectedAgentEmbedToken() === '' ) {
\add_settings_error( 'agent001', 'save_error', \__( 'The selected agent has no widget token. Refresh the agent list or check the agent settings in Agent001, then try again.', 'agent001-support-assistant-for-woocommerce' ) );
return;
}
$selectionChanged = ( $newCategoryIds !== $previousCategoryIds )
|| ( $newAgentIds !== $previousAgentIds );
$needsFirstSync = $this->onboarding->isActive() && ! $this->onboarding->canFinish();
if ( ( $selectionChanged || $needsFirstSync ) && $this->settings->hasApiKey() && $this->settings->hasSelectedAgents()
&& ! $this->progress->isRunning() ) {
$this->syncRunner->schedule();
}
}
}