0) { $bot = $this->plugin->bots->find($site, $requested); if ($bot !== null) { return $bot; } } return $this->plugin->bots->all($site)[0] ?? null; } /** * What is true right now, from local data only. * * @return array */ public function facts(?array $bot): array { $site = get_current_blog_id(); $attached = $bot !== null ? $this->ui->attached($site, (int) $bot['id']) : []; $resolved = $bot !== null ? $this->plugin->ai()->resolve($bot) : null; return [ 'blog' => $site, 'bot_id' => $bot !== null ? (int) $bot['id'] : 0, 'chat_on' => $this->plugin->modules->isEnabled('chat'), 'knowledge_on' => $this->plugin->modules->isEnabled('knowledge'), 'has_bot' => $bot !== null, 'bot_public' => $bot !== null && $bot['status'] === 'active' && $bot['is_public'] === true, 'ai' => $resolved === null ? 'none' : ($resolved['mode'] === 'demo' ? 'demo' : 'real'), 'ai_ready' => $resolved !== null && $resolved['ready'], 'budget' => ((int) ($this->plugin->ledger->balance($site, 'site')['limit'] ?? 0)) > 0, 'attached' => count($attached), 'usable' => count(array_filter($attached, static fn(array $s): bool => $s['status'] === 'ready' && $s['visibility'] === 'public')), 'tested' => $bot !== null && $this->ui->tested((int) $bot['id']), 'on_site' => $bot !== null && $this->ui->onSite($bot), ]; } /** * The instruction a new assistant starts with when it is created by the guided setup. It is neutral on purpose: when knowledge is * attached, the chat service appends its own fixed rules (answer only from the numbered sources, cite them, never guess), so the * source-only behaviour does not depend on this text, while an assistant with no knowledge is not told to use sources it does not * have. It is only ever written to a new assistant; an existing instruction is never touched. */ public static function starterInstruction(): string { return __("Answer briefly and politely.\nIf you are not sure about something, say so instead of guessing.", 'aiwend'); } // ---------------------------------------------------------------- rendering public function render(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only navigation parameters; no state is changed, and neither is evidence of anything $requested = isset($_GET['assistant']) ? (int) $_GET['assistant'] : 0; $stepParam = Input::get('step') !== '' ? sanitize_key(Input::get('step')) : null; // compared strictly against the fixed list of steps, never printed $bot = $this->focus($requested); $facts = $this->facts($bot); $eval = SetupProgress::evaluate($facts); $step = SetupProgress::stepFor($stepParam, $eval); $titles = self::titles(); $finished = $eval['complete']; $this->out('

' . esc_html($finished ? ($facts['tested'] ? __('Your assistant is set up', 'aiwend') : __('Your assistant is set up but not tested yet', 'aiwend')) : __('Setup progress', 'aiwend')) . '

'); if ($bot !== null) { $all = count($this->plugin->bots->all(get_current_blog_id())); $this->out('

' . esc_html(sprintf(/* translators: %s: assistant name */ __('Guiding: %s', 'aiwend'), (string) $bot['name'])) . ($all > 1 ? ' · ' . esc_html__('All assistants', 'aiwend') . '' : '') . '

'); } $doneCount = count(array_filter(SetupProgress::ORDER, static fn(string $k): bool => in_array($eval['steps'][$k]['state'], ['done', 'demo'], true))); $this->out('

' . esc_html(sprintf(/* translators: 1: number of setup steps finished, 2: total number of steps */ __('%1$d of %2$d steps finished', 'aiwend'), $doneCount, count(SetupProgress::ORDER))) . ' ' . (int) $doneCount . '/' . count(SetupProgress::ORDER) . '

'); if ($finished && $stepParam === null) { $this->panelOpen('done', $titles['done']); $this->stepDone($bot, $facts); $this->out(''); $this->out('
' . esc_html__('Review the setup steps', 'aiwend') . ''); $this->stepper($eval, $step, $bot, $titles); $this->out('
'); return; } $this->stepper($eval, $step, $bot, $titles); $this->panelOpen($step, $titles[$step]); match ($step) { 'modules' => $this->stepModules(), 'assistant' => $this->stepAssistant($bot), 'ai' => $this->stepAi($bot, $facts), 'budget' => $this->stepBudget($bot), 'knowledge' => $this->stepKnowledge($bot, $eval), 'test' => $this->stepTest($bot, $facts), 'publish' => $this->stepPublish($bot, $facts), default => $this->stepDone($bot, $facts), }; $this->navigation($step, $eval, $bot); $this->out(''); } /** @return array */ private static function titles(): array { return [ 'modules' => __('Turn on Chat and Knowledge', 'aiwend'), 'assistant' => __('Create the first assistant', 'aiwend'), 'ai' => __('Connect AI', 'aiwend'), 'budget' => __('Set a usage limit', 'aiwend'), 'knowledge' => __('Add knowledge', 'aiwend'), 'test' => __('Test privately', 'aiwend'), 'publish' => __('Publish and add to your site', 'aiwend'), 'done' => __('Setup summary', 'aiwend'), ]; } /** @return array */ private static function summaries(): array { return [ 'modules' => __('Chat shows the assistant; Knowledge lets it answer from your content.', 'aiwend'), 'assistant' => __('A private draft with a safe starting instruction.', 'aiwend'), 'ai' => __('Your own AI provider key, or the local simulated demo.', 'aiwend'), 'budget' => __('Assistants refuse to answer until a limit exists.', 'aiwend'), 'knowledge' => __('Pages, posts or files it may answer from. Optional for a general-purpose assistant.', 'aiwend'), 'test' => __('Ask questions in a preview only you can see.', 'aiwend'), 'publish' => __('Make it Active and Public, then place it on a page.', 'aiwend'), ]; } /** Why a step is locked, in words (SetupProgress gives a code). */ private static function reasonText(string $code): string { return match ($code) { 'needs_modules' => __('Turn on Chat and Knowledge first.', 'aiwend'), 'needs_assistant' => __('Create the first assistant first.', 'aiwend'), 'needs_ai' => __('Connect AI first, or use the simulated demo.', 'aiwend'), 'needs_budget' => __('Set a usage limit first.', 'aiwend'), 'needs_knowledge' => __('Its attached knowledge must be ready and approved first (or detach it to make a general-purpose assistant).', 'aiwend'), default => __('Finish the steps above first.', 'aiwend'), }; } private function stepper(array $eval, string $shown, ?array $bot, array $titles): void { $summaries = self::summaries(); $this->out(''); } private function panelOpen(string $step, string $title): void { $this->out('

' . esc_html($title) . '

'); } private function navigation(string $step, array $eval, ?array $bot): void { $i = array_search($step, SetupProgress::ORDER, true); if ($i === false) { return; } $this->out('

'); if ($i > 0 && $eval['steps'][SetupProgress::ORDER[$i - 1]]['state'] !== 'locked') { $this->out('← ' . esc_html__('Back', 'aiwend') . ' '); } $next = SetupProgress::ORDER[$i + 1] ?? null; if ($next !== null && $eval['steps'][$next]['state'] !== 'locked') { $this->out('' . esc_html__('Next', 'aiwend') . ' →'); } $this->out('

'); } private function stepUrl(string $step, ?array $bot): string { return AdminView::url('aiwend', ['step' => $step] + ($bot !== null ? ['assistant' => (int) $bot['id']] : [])) . '#aw-setup-panel'; } /** @return array hidden fields that bring the administrator back to the same step of the same assistant */ private function back(string $step, ?array $bot): array { return ['step' => $step] + ($bot !== null ? ['assistant' => (string) $bot['id']] : []); } // ---------------------------------------------------------------- the steps private function stepModules(): void { $this->out('

' . esc_html__('Aiwend is made of modules. Chat shows the assistant on your pages; Knowledge lets it answer from your content. Both are off after installation. Turning them on does not show anything to visitors: visitors see nothing until you publish an assistant.', 'aiwend') . '

'); $this->out('
    '); $both = true; foreach (SetupProgress::MODULES as $id) { $module = $this->plugin->modules->get($id); $on = $this->plugin->modules->isEnabled($id); $both = $both && $on; $this->out('
  • ' . esc_html($module->label()) . ' ' . AdminView::badge($on ? __('On', 'aiwend') : __('Off', 'aiwend'), $on ? 'success' : 'neutral') . (!$on && !$module->prerequisitesMet() ? ' ' . esc_html($module->prerequisiteMessage()) . '' : '') . '
  • '); } $this->out('
'); if ($both) { $this->out('

' . esc_html__('Chat and Knowledge are on.', 'aiwend') . '

'); return; } if (!$this->can('modules.manage')) { $this->out('

' . esc_html__('Ask a site administrator to turn them on.', 'aiwend') . '

'); return; } AdminView::form('setup_enable', 'aiwend'); $this->out('

'); $this->out('

' . esc_html__('If only one of them can be turned on, this page says which one and why. You can turn each module on or off at any time under Settings.', 'aiwend') . '

'); } private function stepAssistant(?array $bot): void { if ($bot !== null) { $this->out('

' . esc_html(sprintf(/* translators: %s: assistant name */ __('Your assistant is “%s”.', 'aiwend'), (string) $bot['name'])) . ' ' . $this->lifecycleBadges($bot) . '

'); $this->out('

' . esc_html__('Edit this assistant', 'aiwend') . '

'); return; } $this->out('

' . esc_html__('Give your assistant a name. It starts as a private draft with a short, neutral instruction (be brief and polite, and say so when unsure). When you attach knowledge, Aiwend adds its own rules so answers come only from your sources, with citations. You can change the instruction, the look and everything else later under Assistants.', 'aiwend') . '

'); if (!$this->can('bots.manage')) { $this->out('

' . esc_html__('You do not have permission to create assistants.', 'aiwend') . '

'); return; } AdminView::form('setup_assistant', 'aiwend'); $default = mb_substr(sprintf(/* translators: %s: site name */ __('%s assistant', 'aiwend'), wp_specialchars_decode((string) get_bloginfo('name'), ENT_QUOTES)), 0, 80); $this->out('

' . esc_html__('For you. Visitors see the display name you choose under Appearance.', 'aiwend') . '

'); $this->out('

' . esc_html__('New assistants start as Draft and Private: nobody else can see them until you publish.', 'aiwend') . '

'); $this->out('

'); } private function stepAi(?array $bot, array $facts): void { if ($bot === null) { return; } $site = get_current_blog_id(); $res = $this->plugin->ai()->resolve($bot); $aiUrl = AdminView::url('aiwend-bots', ['assistant' => (int) $bot['id'], 'tab' => 'ai', 'aiwend_setup' => (int) $bot['id']]); if ($res['ready'] && $res['mode'] === 'demo') { $this->out('

' . esc_html__('Simulated demo', 'aiwend') . ' ' . esc_html__('The answers of this assistant are put together by a built-in stand-in from your knowledge. It contacts no AI service and costs nothing. This is not a real AI connection and it is available on local test sites only. You can finish the setup with it, but do not rely on it for visitors.', 'aiwend') . '

'); } elseif ($res['ready']) { $this->out('

' . esc_html__('Ready', 'aiwend') . ' ' . esc_html(sprintf(/* translators: 1: AI provider name, 2: model name */ __('This assistant uses %1$s, model %2$s. The provider accepted the key and listed its models when access was last checked. That does not prove balance or availability; the first real answer comes from your private test.', 'aiwend'), Providers::name((string) $res['provider']), (string) $res['model'])) . '

'); } else { $this->out('

' . esc_html__('Not ready', 'aiwend') . ' ' . esc_html(AiSetup::problemText($res['problem'])) . '

'); } $this->out('

' . esc_html__('Nothing is sent to an AI service while you set up. Aiwend only looks at the settings saved on this site; the first message to a provider is the one you send yourself in the private test.', 'aiwend') . '

'); if (!$res['ready'] || $res['mode'] === 'demo') { $this->out('

' . esc_html__('Use a real AI service', 'aiwend') . '

'); $this->out('

' . esc_html__('Aiwend uses an API key from the provider you choose (OpenAI, Anthropic, Google Gemini, DeepSeek, xAI, OpenRouter or GroqCloud). Answers and their cost go through your own provider account. You can skip this for now and come back: you can still set the limit and prepare your knowledge.', 'aiwend') . '

'); $repo = $this->plugin->connections(); if ($repo === null) { $this->out('

' . esc_html__('First, the site needs an encryption key.', 'aiwend') . ' ' . esc_html__('Your provider key is stored encrypted on this site. The key comes from the secret keys and salts in wp-config.php, so nothing has to be created; if those are missing or still the sample text, the AI Connections screen explains how to add AIWEND_SECRET_KEY to wp-config.php.', 'aiwend') . '

'); $this->out('

' . esc_html__('Check the encryption key', 'aiwend') . '

'); } elseif ($repo->list($site) === []) { $this->out('
  1. ' . esc_html__('Add an AI connection: choose the provider and paste its API key.', 'aiwend') . '
  2. ' . esc_html__('Check access (this asks the provider for its model list; it does not generate an answer).', 'aiwend') . '
  3. ' . esc_html__('Use the “Back to guided setup” button at the top of that screen to return here, then choose the model for this assistant.', 'aiwend') . '
'); $this->out('

' . esc_html__('Add AI connection', 'aiwend') . '

'); } else { $this->out('

' . esc_html__('You have an AI connection. Choose the connection and model this assistant should use; that is a separate, explicit confirmation because it sends visitors’ messages to the provider.', 'aiwend') . '

'); $this->out('

' . esc_html__('Choose the AI and model', 'aiwend') . ' ' . esc_html__('Manage AI connections', 'aiwend') . '

'); } } if (!$facts['ai_ready'] || $res['mode'] === 'demo') { $this->out('

' . esc_html__('Continue without a real AI for now', 'aiwend') . ' →

'); } } private function stepBudget(?array $bot): void { $site = get_current_blog_id(); $limit = (int) ($this->plugin->ledger->balance($site, 'site')['limit'] ?? 0); $this->out('

' . esc_html__('The site limit is the most all your assistants can use in total. When it is used up, assistants stop answering until you raise it. Until a limit exists, assistants refuse to answer at all.', 'aiwend') . '

'); $this->out('

' . esc_html__('A unit is roughly one token of text.', 'aiwend') . ' ' . esc_html__('Units are not money, and this limit is not a spending cap: it counts what Aiwend sends, and your AI provider bills the real usage. Also set a spending limit in your provider account.', 'aiwend') . '

'); $session = (int) $this->plugin->options->get('aiwend_session_budget_units', 20000); $daily = ClientBudget::dailyUnits($this->plugin->options->get(ClientBudget::OPTION_DAILY)); $this->out('

' . esc_html(sprintf(/* translators: 1: units per conversation, 2: units per visitor per day */ __('Already in place: each conversation is limited to %1$s units and each visitor to %2$s units a day. You can change both under Usage.', 'aiwend'), number_format_i18n($session), number_format_i18n($daily))) . '

'); if ($limit > 0) { $this->out('

' . esc_html(sprintf(/* translators: %s: site limit in units */ __('Current site limit: %s units.', 'aiwend'), number_format_i18n($limit))) . ' ' . esc_html(sprintf(/* translators: 1: lowest estimate, 2: highest estimate of answers; both are numbers */ __('At roughly %3$s to %4$s units per answer that allows about %1$s to %2$s answers (an estimate).', 'aiwend'), number_format_i18n((int) floor($limit / self::ANSWER_UNITS_HIGH)), number_format_i18n((int) floor($limit / self::ANSWER_UNITS_LOW)), number_format_i18n(self::ANSWER_UNITS_LOW), number_format_i18n(self::ANSWER_UNITS_HIGH))) . '

'); } if (!$this->can('settings.manage')) { $this->out('

' . esc_html__('Only a site administrator can set the limit.', 'aiwend') . '

'); return; } AdminView::form('setup_budget', 'aiwend', $this->back('budget', $bot)); $this->out('

0 ? ' value="' . (int) $limit . '"' : '') . ' aria-describedby="aw-setup-limit-desc">' . esc_html__('A whole number greater than 0. The Usage screen shows what has been used.', 'aiwend') . '

'); $this->out('

'); } private function stepKnowledge(?array $bot, array $eval): void { if ($bot === null) { return; } $site = get_current_blog_id(); $botId = (int) $bot['id']; $this->out('

' . esc_html__('Choose the pages, posts or files this assistant may answer from. Only published pages and posts without a password can be added.', 'aiwend') . '

'); $this->out('

' . esc_html__('Two kinds of assistant', 'aiwend') . '

  • ' . esc_html__('Knowledge-grounded', 'aiwend') . ' — ' . esc_html__('answers only from the sources attached below, and says it cannot find an answer when they do not cover the question. It needs at least one source that is ready and approved (Public).', 'aiwend') . '
  • ' . esc_html__('General-purpose', 'aiwend') . ' — ' . esc_html__('with no knowledge attached it answers from the AI model’s general knowledge and its instruction, not from your content. Visitors may get answers that have nothing to do with your site.', 'aiwend') . '
'); if ($this->ui->attached(get_current_blog_id(), (int) $bot['id']) === []) { $this->out('

' . esc_html__('No knowledge is attached, so this would be a general-purpose assistant.', 'aiwend') . '

'); if ((string) $bot['instruction'] === $this->ui->exampleInstruction()) { $this->out('

' . esc_html__('Its instruction currently tells it to answer only from knowledge sources, so with none attached it will mostly say it cannot find an answer. Add knowledge below, or change the instruction for a general-purpose assistant.', 'aiwend') . ' ' . esc_html__('Edit the assistant', 'aiwend') . '

'); } foreach (['test', 'publish'] as $next) { if ($eval['steps'][$next]['state'] !== 'locked') { $this->out('

' . esc_html__('Continue without knowledge', 'aiwend') . ' →

'); break; } } } $this->out('

' . esc_html__('Private or Public?', 'aiwend') . '

  • ' . esc_html__('Private', 'aiwend') . ' — ' . esc_html__('stored for you only and not used in answers. This is the starting choice for everything you add.', 'aiwend') . '
  • ' . esc_html__('Public', 'aiwend') . ' — ' . esc_html__('the assistant may quote it to anyone who can chat with it. This does not publish the page or file anywhere else. It always needs your explicit confirmation.', 'aiwend') . '
'); $attached = $this->ui->attached($site, $botId); $readiness = AdminUi::knowledgeReadiness($attached); if ($attached !== []) { $this->out('

' . esc_html__('Knowledge for this assistant', 'aiwend') . '

'); $this->out('

' . esc_html($readiness['text']) . '

'); $this->out('
'); $now = time(); foreach ($attached as $s) { $state = AdminUi::sourceState($s, $now); $action = ''; if (in_array($s['status'], ['failed', 'ocr_required'], true) || ($s['status'] === 'ready' && (string) ($s['error'] ?? '') !== '')) { ob_start(); AdminView::form('reindex_source', 'aiwend', $this->back('knowledge', $bot) + ['id' => (string) $s['id']]); echo ''; $action = (string) ob_get_clean(); } elseif ($s['status'] === 'ready' && $s['visibility'] !== 'public' && $this->can('knowledge.publish')) { $action = '' . esc_html__('Approve for answers', 'aiwend') . ''; } $this->out(''); } $this->out('
' . esc_html__('Source', 'aiwend') . '' . esc_html__('Status', 'aiwend') . '' . esc_html__('What it means', 'aiwend') . '' . esc_html__('Action', 'aiwend') . '
' . esc_html((string) $s['title']) . '' . AdminView::badge($state['label'], $state['tone']) . '' . esc_html($state['reason']) . '' . $action . '
'); $last = (int) get_option(CronDiagnostics::LAST_RUN_OPTION, 0); $this->out('

' . esc_html__('New content is prepared in the background by WordPress’s scheduled tasks (WP-Cron), usually within a minute or two of a visit to your site. It can take longer on a quiet site or when WP-Cron is turned off.', 'aiwend') . ' ' . esc_html($last > 0 ? sprintf(/* translators: %s: time span since the last background run, for example "5 minutes" */ __('Background jobs last ran %s ago.', 'aiwend'), human_time_diff($last)) : __('Background jobs have not run yet.', 'aiwend')) . ' ' . esc_html__('Open diagnostics', 'aiwend') . '

'); $this->out('

' . esc_html__('Refresh status', 'aiwend') . '

'); } $ids = array_map(static fn(array $s): int => (int) $s['id'], $attached); $library = array_values(array_filter($this->plugin->sources()->list($site), static fn(array $s): bool => !in_array((int) $s['id'], $ids, true) && !in_array($s['status'], ['deleting', 'deleted'], true))); if ($library !== [] && $this->can('bots.manage')) { $this->out('

' . esc_html__('Already in your library', 'aiwend') . '

'); AdminView::form('attach_sources', 'aiwend', $this->back('knowledge', $bot)); $this->out('
' . esc_html__('Library sources to attach', 'aiwend') . '
    '); foreach (array_slice($library, 0, 50) as $s) { $this->out('
  • '); } $this->out('

'); } if ($this->can('knowledge.manage')) { $this->out('

' . esc_html__('Add content', 'aiwend') . '

'); $this->ui->addContentPanels('aiwend', $this->back('knowledge', $bot), $attached === []); } } private function stepTest(?array $bot, array $facts): void { if ($bot === null) { return; } $this->out('

' . esc_html__('Ask your assistant a few questions in a preview that only you can see. Visitors cannot reach it, and testing never publishes the assistant.', 'aiwend') . '

'); if ($facts['ai'] === 'real') { $this->out('

' . esc_html__('With a real AI connection, each test question is sent to your AI provider and counts against your limit. Nothing is sent until you ask a question in the preview.', 'aiwend') . '

'); } else { $this->out('

' . esc_html__('This assistant uses the simulated demo, so the answers are stand-ins and nothing is sent to an AI service.', 'aiwend') . '

'); } $this->out('

' . AdminView::badge($facts['tested'] ? __('Tested: a private test answer worked', 'aiwend') : __('Not tested yet', 'aiwend'), $facts['tested'] ? 'success' : 'neutral') . '

'); $this->out('

' . esc_html__('Open the private preview', 'aiwend') . ' ' . esc_html__('Refresh status', 'aiwend') . '

'); $this->out('

' . esc_html__('Testing is recommended but not required. In the preview, switch on “Test conversation” to send real messages.', 'aiwend') . '

'); } private function stepPublish(?array $bot, array $facts): void { if ($bot === null) { return; } $id = (int) $bot['id']; $live = $facts['bot_public']; if ($facts['ai'] === 'demo') { $this->out('

' . esc_html__('Simulated demo', 'aiwend') . ' ' . esc_html__('This assistant answers with the built-in stand-in, not with a real AI. Visitors would get stand-in answers: use this for trying things out only.', 'aiwend') . '

'); } foreach ($this->ui->blockers($bot) as [$text, $url, $label]) { $this->out(''); } if ($facts['tested'] === false && !$live) { $this->out('

' . esc_html__('You have not tested this assistant privately yet. That is recommended before publishing.', 'aiwend') . ' ' . esc_html__('Test privately', 'aiwend') . '

'); } $general = (int) $facts['attached'] === 0; if ($general) { $this->out('

' . esc_html__('General-purpose assistant', 'aiwend') . ' ' . esc_html__('No knowledge is attached, so it answers from the AI model’s general knowledge and its instruction, not from your content. Visitors may get answers that have nothing to do with your site. Go back to Add knowledge if you want answers from your own pages.', 'aiwend') . '

'); } $this->out('

' . $this->lifecycleBadges($bot) . '

'); if (!$live && $this->can('bots.manage')) { $this->out('

' . esc_html__('Publishing makes the assistant Active and Public. Visitors can then use it on any page where you add it; until you add it to a page, nobody sees it.', 'aiwend') . '

'); AdminView::form('setup_publish', 'aiwend', $this->back('publish', $bot)); $this->out(''); $this->out('

'); } $this->out('

' . esc_html__('Add it to a page', 'aiwend') . '

'); $this->out('

' . esc_html__('In the block editor, add the “Aiwend Chat” block and choose this assistant, or paste this shortcode into a page or post:', 'aiwend') . '

'); $code = '[aiwend_chat bot="' . $id . '"]'; $this->out('
'); if (current_user_can('edit_pages')) { AdminView::form('create_test_page', 'aiwend', $this->back('publish', $bot)); $this->out('

' . esc_html__('Creates an unpublished page you can open, check in your theme and publish yourself.', 'aiwend') . '

'); } if ($live) { $this->out('

' . AdminView::badge($facts['on_site'] ? __('Placed on a published page', 'aiwend') : __('Not on any published page yet', 'aiwend'), $facts['on_site'] ? 'success' : 'warn') . ' ' . esc_html__('Refresh status', 'aiwend') . '

'); } else { $this->out('

' . esc_html__('Until you publish the assistant above, visitors will not see it even on a page that contains it.', 'aiwend') . '

'); } } private function stepDone(?array $bot, array $facts): void { if ($bot === null) { return; } $res = $this->plugin->ai()->resolve($bot); $limit = (int) ($this->plugin->ledger->balance(get_current_blog_id(), 'site')['limit'] ?? 0); $this->out('

' . esc_html(sprintf(/* translators: %s: assistant name */ __('“%s” is published and placed on a page.', 'aiwend'), (string) $bot['name'])) . '

'); $this->out('
    '); $this->out('
  • ' . ($facts['ai'] === 'demo' ? '' . esc_html__('It answers with the simulated demo.', 'aiwend') . ' ' . esc_html__('No real AI service is used and nothing is charged. This is not a production setup.', 'aiwend') : esc_html(sprintf(/* translators: 1: AI provider name, 2: model name */ __('AI: %1$s, model %2$s.', 'aiwend'), Providers::name((string) $res['provider']), (string) $res['model']))) . '
  • '); $this->out('
  • ' . esc_html((int) $facts['attached'] === 0 ? __('Knowledge: none attached. It is a general-purpose assistant and answers from the AI model’s general knowledge, not from your content.', 'aiwend') : sprintf(/* translators: 1: number of knowledge sources used in answers, 2: number attached */ __('Knowledge: %1$d of %2$d attached sources are used in answers.', 'aiwend'), (int) $facts['usable'], (int) $facts['attached'])) . '
  • '); $this->out('
  • ' . esc_html(sprintf(/* translators: %s: site limit in units */ __('Usage limit: %s units for the whole site.', 'aiwend'), number_format_i18n($limit))) . '
'); $this->out('

' . ($this->plugin->modules->isEnabled('chat') ? '' . esc_html__('See conversations', 'aiwend') . ' ' : '') . '' . esc_html__('Review usage', 'aiwend') . ' ' . esc_html__('Manage knowledge', 'aiwend') . ' ' . esc_html__('Edit the assistant', 'aiwend') . '

'); $this->out('

' . esc_html__('Keep an eye on usage in your AI provider account too. Aiwend’s limit counts units, not money.', 'aiwend') . '

'); } // ---------------------------------------------------------------- small helpers private function lifecycleBadges(array $bot): string { $status = match ($bot['status']) { 'active' => AdminView::badge(__('Active', 'aiwend'), 'success'), 'paused' => AdminView::badge(__('Paused', 'aiwend'), 'warn'), default => AdminView::badge(__('Draft', 'aiwend'), 'neutral'), }; return $status . ' ' . ($bot['is_public'] ? AdminView::badge(__('Public', 'aiwend'), 'brand') : AdminView::badge(__('Private', 'aiwend'), 'neutral')); } private function can(string $operation): bool { return $this->plugin->policy->check($this->plugin->currentPrincipal(), $operation)->allowed; } /** Output of markup built from escaped parts, integers and fixed labels, filtered once more through the admin allowlist (wp_kses). */ private function out(string $html): void { echo wp_kses($html, AdminView::allowedHtml()); // the markup is built from escaped parts; the allowlist is the second barrier } }