', esc_attr($opt), Settings::canConfigureStores() ? '' : ' disabled'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- literal attribute foreach (StoreFactory::available() as $id => $label) { printf('', esc_attr($id), selected($saved, $id, false), esc_html($label)); } echo ''; self::moveNote($saved); $line = Settings::canConfigureStores() ? __('Where the vectors are kept. Vectorize uses your Cloudflare account; Qdrant and PostgreSQL are servers you run or rent. Save, then use Test connection.', 'aipcf-ai-provider-for-cloudflare') : __('Only a network administrator can change the vector store and its connection on this network.', 'aipcf-ai-provider-for-cloudflare'); echo FreeTierSettings::help($line); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes }, self::PAGE, 'aipcf_embed_store', ['label_for' => 'aipcf-embed-store']); add_settings_field('vectorize_index', __('Vectorize index name', 'aipcf-ai-provider-for-cloudflare'), static function () use ($opt): void { $saved = (string) (get_plugin_settings()['vectorize_index'] ?? ''); printf( '', esc_attr($opt), esc_attr($saved), /* translators: %s: default name */ esc_attr(sprintf(__('Default: %s', 'aipcf-ai-provider-for-cloudflare'), VectorizeStore::sanitizeIndexName('aipcf-' . (string) wp_parse_url(home_url(), PHP_URL_HOST)))), self::disabled('vectorize') // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- literal attribute ); echo FreeTierSettings::help(__('Leave empty for one index per site. Lowercase letters, digits and dashes.', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes }, self::PAGE, 'aipcf_embed_store', self::rowArgs('vectorize', true, 'aipcf-vectorize-index')); // Qdrant self::text('qdrant_url', __('Qdrant URL', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_embed_store', __('e.g. https://xyz.cloud.qdrant.io:6333', 'aipcf-ai-provider-for-cloudflare'), __('Qdrant Cloud or your own server, including the port. Content is sent here. Use https unless it runs on this machine.', 'aipcf-ai-provider-for-cloudflare'), 'qdrant'); self::secret('qdrant_api_key', __('Qdrant API key', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_embed_store', 'AIPCF_QDRANT_API_KEY', 'qdrant'); self::text('qdrant_collection', __('Collection name', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_embed_store', sprintf(/* translators: %s: default name */ __('Default: %s', 'aipcf-ai-provider-for-cloudflare'), QdrantStore::sanitizeName('aipcf-' . (string) wp_parse_url(home_url(), PHP_URL_HOST))), __('Leave empty for one collection per site.', 'aipcf-ai-provider-for-cloudflare'), 'qdrant', true); // PostgreSQL + pgvector add_settings_field('pg_requirements', __('Requirements', 'aipcf-ai-provider-for-cloudflare'), static function (): void { echo PgvectorStore::available() // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- literal HTML with escaped text ? '

' . esc_html__('This server\'s PHP can connect to PostgreSQL (pdo_pgsql is installed). The database needs the pgvector extension.', 'aipcf-ai-provider-for-cloudflare') . '

' : '

' . esc_html__('This server\'s PHP cannot connect to PostgreSQL: the pdo_pgsql extension is not installed.', 'aipcf-ai-provider-for-cloudflare') . ' ' . esc_html__('Ask your host to enable it, or choose Vectorize or Qdrant.', 'aipcf-ai-provider-for-cloudflare') . '

'; }, self::PAGE, 'aipcf_embed_store', self::rowArgs('pgvector')); self::text('pg_host', __('Host', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_embed_store', __('e.g. db.example.com', 'aipcf-ai-provider-for-cloudflare'), '', 'pgvector'); self::number('pg_port', __('Port', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_embed_store', '', 1, 65535, 'pgvector'); self::text('pg_database', __('Database', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_embed_store', __('e.g. wordpress_vectors', 'aipcf-ai-provider-for-cloudflare'), '', 'pgvector'); self::text('pg_user', __('User', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_embed_store', __('e.g. wordpress', 'aipcf-ai-provider-for-cloudflare'), '', 'pgvector'); self::secret('pg_password', __('Password', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_embed_store', 'AIPCF_PG_PASSWORD', 'pgvector'); add_settings_field('pg_sslmode', __('SSL', 'aipcf-ai-provider-for-cloudflare'), static function () use ($opt): void { $saved = (string) (get_plugin_settings()['pg_sslmode'] ?? 'prefer'); printf(''; echo FreeTierSettings::help(__('"require" or stricter for a database on another machine.', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes }, self::PAGE, 'aipcf_embed_store', self::rowArgs('pgvector', true, 'aipcf-pg-sslmode')); self::text('pg_table', __('Table name', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_embed_store', sprintf(/* translators: %s: default name */ __('Default: %s', 'aipcf-ai-provider-for-cloudflare'), PgvectorStore::sanitizeName('aipcf_' . (string) wp_parse_url(home_url(), PHP_URL_HOST))), __('Leave empty for one table per site.', 'aipcf-ai-provider-for-cloudflare'), 'pgvector', true); add_settings_section('aipcf_embed_model', __('2. Embedding model', 'aipcf-ai-provider-for-cloudflare'), static function (): void { echo '

' . esc_html__('The model that turns text into vectors. Changing it means every item is embedded again. If the new model\'s vectors have a different size, press Rebuild (under More actions) to start a new index; with a custom index name, enter a new name instead.', 'aipcf-ai-provider-for-cloudflare') . '

'; }, self::PAGE, SettingsPage::panel()); add_settings_field('embed_model', __('Model', 'aipcf-ai-provider-for-cloudflare'), static function () use ($opt): void { $saved = Models::current(); printf(''; echo FreeTierSettings::help(__('BGE-M3 is the default: multilingual, the cheapest, and reads up to 8,192 tokens. The English-only BGE models read at most 512.', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes }, self::PAGE, 'aipcf_embed_model', ['label_for' => 'aipcf-embed-model']); add_settings_section('aipcf_embed_select', __('3. What to embed', 'aipcf-ai-provider-for-cloudflare'), static function (): void { echo '

' . esc_html__('Only what is selected here is sent to Workers AI and stored. Password-protected posts are never included, and any post can be left out from its editor.', 'aipcf-ai-provider-for-cloudflare') . '

'; }, self::PAGE, SettingsPage::panel()); add_settings_field('embed_post_types', __('Content types', 'aipcf-ai-provider-for-cloudflare'), static function () use ($opt): void { printf('
%s', esc_html(__('Content types', 'aipcf-ai-provider-for-cloudflare'))); $saved = Settings::postTypes(); foreach (Settings::availablePostTypes() as $type) { $obj = get_post_type_object($type); printf( '', esc_attr($opt), esc_attr($type), checked(in_array($type, $saved, true), true, false), esc_html($obj ? (string) $obj->labels->name : $type) ); } printf( '

', esc_attr($opt), checked(Settings::includeAttachments(), true, false), esc_html__('Images (their title, alt text, caption and description)', 'aipcf-ai-provider-for-cloudflare') ); echo '
'; }, self::PAGE, 'aipcf_embed_select'); add_settings_field('embed_statuses', __('Include', 'aipcf-ai-provider-for-cloudflare'), static function () use ($opt): void { printf('
%s', esc_html(__('Include', 'aipcf-ai-provider-for-cloudflare'))); $saved = Settings::statuses(); printf('', esc_attr($opt), checked(in_array('publish', $saved, true), true, false), esc_html__('Published', 'aipcf-ai-provider-for-cloudflare')); printf('', esc_attr($opt), checked(in_array('private', $saved, true), true, false), esc_html__('Private', 'aipcf-ai-provider-for-cloudflare')); echo FreeTierSettings::help(__('Private content is readable by anyone who can read the vector store.', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes echo '
'; }, self::PAGE, 'aipcf_embed_select'); add_settings_field('embed_fields', __('Fields', 'aipcf-ai-provider-for-cloudflare'), static function () use ($opt): void { printf('
%s', esc_html(__('Fields', 'aipcf-ai-provider-for-cloudflare'))); $saved = Settings::fields(); foreach (Settings::fieldLabels() as $key => $label) { printf( '', esc_attr($opt), esc_attr($key), checked(in_array($key, $saved, true), true, false), esc_html($label) ); } echo '
'; }, self::PAGE, 'aipcf_embed_select'); add_settings_field('embed_meta_keys', __('Custom fields', 'aipcf-ai-provider-for-cloudflare'), static function () use ($opt): void { printf('', esc_attr($opt), esc_attr(implode(', ', Settings::metaKeys())), esc_attr__('e.g. price, location', 'aipcf-ai-provider-for-cloudflare')); echo FreeTierSettings::help(__('Meta keys to include when "Custom fields" is ticked, separated by commas. Text values only.', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes }, self::PAGE, 'aipcf_embed_select', FreeTierSettings::fieldArgs(true) + ['label_for' => 'aipcf-embed-meta-keys']); add_settings_field('embed_tax_mode', __('Categories and tags', 'aipcf-ai-provider-for-cloudflare'), static function () use ($opt): void { printf('
%s', esc_html(__('Categories and tags', 'aipcf-ai-provider-for-cloudflare'))); $s = get_plugin_settings(); $mode = (string) ($s['embed_tax_mode'] ?? 'none'); $modes = [ 'none' => __('Everything', 'aipcf-ai-provider-for-cloudflare'), 'include' => __('Only these', 'aipcf-ai-provider-for-cloudflare'), 'exclude' => __('All except these', 'aipcf-ai-provider-for-cloudflare'), ]; foreach ($modes as $value => $label) { printf('', esc_attr($opt), esc_attr($value), checked($mode, $value, false), esc_html($label)); } $terms = array_map(static fn($t): string => strpos((string) $t, 'category:') === 0 ? substr((string) $t, 9) : (string) $t, (array) ($s['embed_terms'] ?? [])); printf('

', esc_attr($opt), esc_attr(implode(', ', $terms)), esc_attr__('e.g. sponsored, post_tag:ads', 'aipcf-ai-provider-for-cloudflare'), esc_attr__('Categories or tags to include or leave out', 'aipcf-ai-provider-for-cloudflare')); echo FreeTierSettings::help(__('Category slugs, or taxonomy:slug for tags and other taxonomies. Types without these taxonomies (such as pages) are not filtered.', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes echo '
'; }, self::PAGE, 'aipcf_embed_select', FreeTierSettings::fieldArgs(true)); add_settings_field('embed_after', __('Published after', 'aipcf-ai-provider-for-cloudflare'), static function () use ($opt): void { printf('', esc_attr($opt), esc_attr(Settings::after())); echo FreeTierSettings::help(__('Leave empty for everything. Useful to skip an old archive.', 'aipcf-ai-provider-for-cloudflare')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes }, self::PAGE, 'aipcf_embed_select', FreeTierSettings::fieldArgs(true) + ['label_for' => 'aipcf-embed-after']); self::number('embed_min_words', __('Shortest post', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_embed_select', __('Posts and pages with fewer words are left out of the content index. Very short ones (a link, a quick note) then cost nothing and do not show up as related posts or search matches. 0 embeds everything. Images are not counted.', 'aipcf-ai-provider-for-cloudflare'), 0, 5000, '', __('words', 'aipcf-ai-provider-for-cloudflare')); self::checkbox('embed_store_text', __('Store the text', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_embed_select', __('Keep each passage\'s text next to its vector', 'aipcf-ai-provider-for-cloudflare'), true, __('Used for the snippets in Test search and in Search (when a site shows them) and for the quotes in the Link assistant.', 'aipcf-ai-provider-for-cloudflare')); add_settings_section('aipcf_embed_estimate', __('4. Estimate and switch on', 'aipcf-ai-provider-for-cloudflare'), [self::class, 'renderEstimate'], self::PAGE, SettingsPage::panel()); self::checkbox('embeddings_enabled', __('Embeddings', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_embed_estimate', __('On: embed the content selected above and keep the vector store in sync', 'aipcf-ai-provider-for-cloudflare')); self::number('embed_daily_neurons', __('Daily limit', 'aipcf-ai-provider-for-cloudflare'), 'aipcf_embed_estimate', __('Pays for embedding new and changed content. 0 means no limit of its own. It also counts against the daily budget.', 'aipcf-ai-provider-for-cloudflare'), 0, 10000000, '', __('neurons a day', 'aipcf-ai-provider-for-cloudflare')); } public static function renderIntro(): void { echo '

' . esc_html__('Beta.', 'aipcf-ai-provider-for-cloudflare') . ' ' . esc_html__('Turns the content you choose into embeddings with a Workers AI model and keeps them in a vector store: the content index. The Site assistant, Related posts, Search, the Link assistant, Search insights and AI agents use it.', 'aipcf-ai-provider-for-cloudflare') . '

'; if (!Settings::enabled()) { echo '

' . esc_html__('Off: no content is sent, and the vector store is only contacted when you press Test connection or Create index. Go through steps 1 to 4 below, check the estimate, then switch it on at the end and save.', 'aipcf-ai-provider-for-cloudflare') . '

'; return; } printf( '

%s %s

', esc_html(sprintf( /* translators: %s: estimated neurons */ __('Embedding neurons used today: about %s.', 'aipcf-ai-provider-for-cloudflare'), number_format_i18n(UsageTracker::todayEmbedNeurons(), 1) )), esc_html__('Workers AI reports no usage for embeddings, so this is estimated from text length.', 'aipcf-ai-provider-for-cloudflare') ); } public static function renderStorePanel(): void { echo '
'; self::renderStoreState(); echo '
'; } /** * What saving another store will do, shown (by settings.js) as soon as one is picked: the * vectors are copied, free, and the old store keeps serving until the move is done. */ private static function moveNote(string $saved): void { if (!Settings::enabled() || !IndexTable::exists()) { return; } $move = State::get()['move']; if ($move !== null) { printf('', esc_attr($saved), esc_attr__('A move to another store is under way. Wait for it to finish, or cancel it in the status panel below; saving another store is ignored until then.', 'aipcf-ai-provider-for-cloudflare')); return; } $c = IndexTable::counts(Settings::signature()); if ($c['indexed'] === 0) { return; } $minutes = max(1, (int) ceil($c['chunks'] / 600)); /* translators: %s: number of minutes */ $time = sprintf(_n('about %s minute', 'about %s minutes', $minutes, 'aipcf-ai-provider-for-cloudflare'), number_format_i18n($minutes)); $attrs = ''; foreach (StoreFactory::available() as $id => $label) { if ($id === $saved) { continue; } $attrs .= sprintf(' data-msg-%s="%s"', esc_attr($id), esc_attr(sprintf( /* translators: 1: number of posts, 2: number of vectors, 3: current store, 4: new store, 5: how long, like "about 2 minutes" */ _n( 'Saving moves %1$s post (%2$s vectors) from %3$s to %4$s: they are copied, no neurons, in %5$s. Related posts and search keep using %3$s until the move is done; afterwards you can delete what is left there.', 'Saving moves %1$s posts (%2$s vectors) from %3$s to %4$s: they are copied, no neurons, in %5$s. Related posts and search keep using %3$s until the move is done; afterwards you can delete what is left there.', $c['indexed'], 'aipcf-ai-provider-for-cloudflare' ), number_format_i18n($c['indexed']), number_format_i18n($c['chunks']), StoreFactory::available()[$saved] ?? $saved, $label, $time ))); } $e = Estimate::get(); printf( '', esc_attr($saved), esc_attr(Models::current()), $attrs, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above esc_attr(sprintf( /* translators: %s: estimated neurons */ __('The model changes too, so nothing can be copied: everything is embedded again in the new store (about %s neurons).', 'aipcf-ai-provider-for-cloudflare'), number_format_i18n((float) ($e['neurons'] ?? 0), 0) )) ); } private static function renderStoreState(): void { $store = StoreFactory::current(); if (is_wp_error($store)) { echo '

' . esc_html($store->get_error_message()) . '

'; return; } // While off, the store is only contacted on request (Test connection / Create index). $r = EmbeddingsStatus::storeStatus($store, Settings::enabled()); $dims = (int) (Models::get(Models::current())['dims'] ?? 0); // Every branch is one full sentence with the index name as a placeholder. The // sentence is escaped first; the name is escaped and wrapped in separately. $name = '' . esc_html($store->indexName()) . ''; echo '

'; if (isset($r['unchecked'])) { /* translators: %s: index name */ printf(esc_html__('Index %s is not checked yet. Press Test connection.', 'aipcf-ai-provider-for-cloudflare'), $name); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above } elseif (isset($r['error'])) { /* translators: 1: index name, 2: error detail */ printf(esc_html__('Index %1$s cannot be read: %2$s', 'aipcf-ai-provider-for-cloudflare'), $name, esc_html((string) $r['error'])); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above } elseif (empty($r['exists'])) { /* translators: %s: index name */ printf(esc_html__('Index %s is not created yet.', 'aipcf-ai-provider-for-cloudflare'), $name); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above } elseif ((int) $r['dims'] !== $dims) { // Rebuild starts a new index only for the site's own index; a custom name is re-embedded in place. printf( $store->isSiteOwn() /* translators: 1: index name, 2: numbers in each vector the index holds, 3: numbers in each vector the model makes */ ? esc_html__('Index %1$s holds vectors of %2$s numbers, but the selected model makes vectors of %3$s. Press Rebuild (under More actions) to start a new index, or go back to a model with the same vector size.', 'aipcf-ai-provider-for-cloudflare') /* translators: 1: index name, 2: numbers in each vector the index holds, 3: numbers in each vector the model makes */ : esc_html__('Index %1$s holds vectors of %2$s numbers, but the selected model makes vectors of %3$s. Enter a new index name, or go back to a model with the same vector size.', 'aipcf-ai-provider-for-cloudflare'), $name, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above esc_html(number_format_i18n((int) $r['dims'])), esc_html(number_format_i18n($dims)) ); } elseif (empty($r['ready'])) { /* translators: %s: index name */ printf(esc_html__('Index %s is being prepared by Cloudflare. This can take a few minutes.', 'aipcf-ai-provider-for-cloudflare'), $name); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above } else { printf( /* translators: 1: index name, 2: vector count, 3: numbers in each vector (e.g. 1,024) */ esc_html(_n('Index %1$s is ready: %2$s vector of %3$s numbers.', 'Index %1$s is ready: %2$s vectors of %3$s numbers.', (int) ($r['vector_count'] ?? 0), 'aipcf-ai-provider-for-cloudflare')), $name, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above esc_html(number_format_i18n((int) ($r['vector_count'] ?? 0))), esc_html(number_format_i18n((int) $r['dims'])) ); if (empty($r['settled'])) { echo ' ' . esc_html__('Recent changes are still being applied (about a minute).', 'aipcf-ai-provider-for-cloudflare'); } } echo '

'; printf( '

%s %s

', esc_url(wp_nonce_url(admin_url('admin-post.php?action=aipcf_embed_test'), 'aipcf_embed_test')), esc_attr__('Testing...', 'aipcf-ai-provider-for-cloudflare'), esc_html__('Test connection', 'aipcf-ai-provider-for-cloudflare'), esc_url(wp_nonce_url(admin_url('admin-post.php?action=aipcf_embed_prepare'), 'aipcf_embed_prepare')), esc_attr__('Checking...', 'aipcf-ai-provider-for-cloudflare'), empty($r['exists']) ? esc_html__('Create index', 'aipcf-ai-provider-for-cloudflare') : esc_html__('Create or check index', 'aipcf-ai-provider-for-cloudflare') ); if ($store->id() === 'vectorize') { $help = FreeTierSettings::help( __('Vectorize uses your Cloudflare token, which needs the Account > Vectorize > Edit permission in addition to Workers AI.', 'aipcf-ai-provider-for-cloudflare'), __('Adding the permission, and the Vectorize free plan', 'aipcf-ai-provider-for-cloudflare'), esc_html__('On dash.cloudflare.com, open My Profile > API Tokens, click "..." > Edit on the token this site uses, add the permission Account > Vectorize > Edit, and save. The token value does not change, so nothing needs updating here. Press Test connection. (If Test connection says "Authentication error", the permission went onto a different token.)', 'aipcf-ai-provider-for-cloudflare') . '

' . esc_html__('The Vectorize free plan (part of Workers Free) stores 5 million dimensions: vectors times the numbers in each, about 4,900 passages at 1,024 numbers. It answers searches over 30 million dimensions a month. Workers Paid includes 10 million stored dimensions, then $0.05 per 100 million.', 'aipcf-ai-provider-for-cloudflare') ); } elseif ($store->id() === 'qdrant') { $help = FreeTierSettings::help(__('Qdrant applies writes at once. Qdrant Cloud has a free 1 GB cluster (about 200,000 passages with vectors of 1,024 numbers).', 'aipcf-ai-provider-for-cloudflare')); } else { $help = FreeTierSettings::help(__('PostgreSQL applies writes at once. The plugin creates one table with a search index.', 'aipcf-ai-provider-for-cloudflare')); } echo $help; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes foreach (Settings::transportWarnings() as $warning) { echo '

' . esc_html($warning) . '

'; } } public static function renderEstimate(): void { echo '
'; $e = Estimate::get(); if ($e['objects'] === 0) { echo '

' . esc_html__('Nothing matches the selection yet.', 'aipcf-ai-provider-for-cloudflare') . '

'; } else { echo '

'; /* translators: %s: number of items */ echo self::count(_n('%s item', '%s items', $e['objects'], 'aipcf-ai-provider-for-cloudflare'), number_format_i18n($e['objects'])); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- count() escapes /* translators: %s: estimated number of words */ echo self::count(_n('%s word', '%s words', (int) ($e['words'] ?? 0), 'aipcf-ai-provider-for-cloudflare'), '~' . number_format_i18n((int) ($e['words'] ?? 0))); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- count() escapes if (!empty($e['short'])) { /* translators: %s: estimated number of posts under the minimum length */ echo self::count(_n('%s too short, left out', '%s too short, left out', (int) $e['short'], 'aipcf-ai-provider-for-cloudflare'), '~' . number_format_i18n((int) $e['short'])); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- count() escapes } /* translators: %s: estimated number of passages */ echo self::count(_n('%s passage (parts of posts)', '%s passages (parts of posts)', $e['chunks'], 'aipcf-ai-provider-for-cloudflare'), '~' . number_format_i18n($e['chunks'])); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- count() escapes /* translators: %s: estimated number of tokens */ echo self::count(_n('%s token', '%s tokens', $e['tokens'], 'aipcf-ai-provider-for-cloudflare'), '~' . number_format_i18n($e['tokens'])); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- count() escapes /* translators: %s: estimated neurons */ echo self::count(_n('%s neuron to embed everything once', '%s neurons to embed everything once', (int) round($e['neurons']), 'aipcf-ai-provider-for-cloudflare'), '~' . number_format_i18n($e['neurons'], 0)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- count() escapes echo '

'; $cap = Settings::dailyNeurons(); echo '

'; if ($cap > 0) { echo esc_html(sprintf( /* translators: 1: days, 2: daily limit in neurons */ _n('About %1$d day at %2$s neurons a day.', 'About %1$d days at %2$s neurons a day.', $e['days'], 'aipcf-ai-provider-for-cloudflare'), $e['days'], number_format_i18n($cap) )) . ' '; } $fits = [ 'free' => __('Fits the Vectorize free plan (5 million stored dimensions).', 'aipcf-ai-provider-for-cloudflare'), 'paid' => __('Too big for the Vectorize free plan. It fits the 10 million included with Workers Paid.', 'aipcf-ai-provider-for-cloudflare'), 'paid_extra' => __('Needs Workers Paid. Storage beyond 10 million dimensions costs $0.05 per 100 million a month.', 'aipcf-ai-provider-for-cloudflare'), ]; if (isset($fits[$e['fits']])) { echo esc_html(sprintf( /* translators: %s: stored dimensions (vectors times the numbers in each) */ __('Storage: about %s dimensions.', 'aipcf-ai-provider-for-cloudflare'), number_format_i18n($e['stored_dims']) )) . ' ' . esc_html($fits[$e['fits']]); } echo '

'; } printf( '

%s %s

', esc_html(sprintf( /* translators: 1: sampled count, 2: time ago */ _n('From %1$d sampled item, %2$s ago. Save to update it after changing the selection.', 'From %1$d sampled items, %2$s ago. Save to update it after changing the selection.', $e['sampled'], 'aipcf-ai-provider-for-cloudflare'), $e['sampled'], human_time_diff($e['computed_at']) )), esc_url(wp_nonce_url(admin_url('admin-post.php?action=aipcf_embed_estimate'), 'aipcf_embed_estimate')), esc_attr__('Calculating...', 'aipcf-ai-provider-for-cloudflare'), esc_html__('Recalculate', 'aipcf-ai-provider-for-cloudflare') ); echo '
'; } /** * One piece of a counts line: the translated format with the number in . * * @param string $format A translated format with one %s for the number. */ public static function count(string $format, string $number): string { return '' . sprintf(esc_html($format), '' . esc_html($number) . '') . ''; } // ================================================================ field helpers private static function checkbox(string $key, string $label, string $section, string $desc, bool $advanced = false, string $help = ''): void { $opt = AIPCF_SETTINGS_OPTION; add_settings_field($key, $label, static function () use ($key, $opt, $desc, $help): void { printf('', esc_attr(self::fieldId($key)), esc_attr($opt), esc_attr($key), checked(!empty(get_plugin_settings()[$key]), true, false), esc_html($desc)); if ($help !== '') { echo FreeTierSettings::help($help); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes } }, self::PAGE, $section, FreeTierSettings::fieldArgs($advanced) + ['label_for' => self::fieldId($key)]); } private static function number(string $key, string $label, string $section, string $desc, int $min, int $max, string $store = '', string $unit = ''): void { $opt = AIPCF_SETTINGS_OPTION; add_settings_field($key, $label, static function () use ($key, $opt, $desc, $min, $max, $store, $unit): void { printf('', esc_attr(self::fieldId($key)), esc_attr($opt), esc_attr($key), (int) (get_plugin_settings()[$key] ?? 0), (int) $min, (int) $max, $store !== '' ? self::disabled($store) : ''); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- literal attribute if ($unit !== '') { echo '' . esc_html($unit) . ''; } if ($desc !== '') { echo FreeTierSettings::help($desc); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- help() escapes } }, self::PAGE, $section, $store !== '' ? self::rowArgs($store, false, self::fieldId($key)) : ['label_for' => self::fieldId($key)]); } /** * Row attributes for one store's field: hidden unless that store is the saved * choice (JavaScript switches them live), locked when the user may not configure stores. * * @return array{class: string} */ private static function rowArgs(string $store, bool $advanced = false, string $labelFor = ''): array { $off = StoreFactory::currentId() !== $store; $args = ['class' => trim(($advanced ? 'aipcf-advanced ' : '') . 'aipcf-store-row aipcf-store-' . $store . ($off ? ' aipcf-store-off' : '') . (Settings::canConfigureStores() ? '' : ' aipcf-store-locked'))]; if ($labelFor !== '') { $args['label_for'] = $labelFor; // the row heading becomes the field's