[ 'label' => __('Meta description (Backfill)', 'aipcf-ai-provider-for-cloudflare'), 'description' => __('System instruction Backfill uses to write a missing SEO meta description. The post title and content are sent as the user message.', 'aipcf-ai-provider-for-cloudflare'), 'placeholders' => ['{site_name}', '{language}', '{max_chars}'], 'model' => __('Runs on the light model, or on the default model when no light model is set.', 'aipcf-ai-provider-for-cloudflare'), 'default' => "You write SEO meta descriptions for {site_name}.\n" . "Write one meta description for the post you are given: a single sentence or two short sentences, between 120 and {max_chars} characters, in {language}, in the same language as the post.\n" . "It must summarise what the post actually says, use the post's main topic naturally, and invite the reader to click. Do not repeat the title, do not stuff keywords, do not use quotes, markdown or emojis.\n" . "Reply with the meta description only.", ], 'alt_text' => [ 'label' => __('Alt text (Backfill)', 'aipcf-ai-provider-for-cloudflare'), 'description' => __('Prompt sent with each image Backfill describes. {title} is the attachment title when it looks meaningful.', 'aipcf-ai-provider-for-cloudflare'), 'placeholders' => ['{title}', '{site_name}', '{max_chars}'], 'model' => __('Runs on the vision model.', 'aipcf-ai-provider-for-cloudflare'), 'default' => "Write alt text for this image for the media library of {site_name}. One sentence, at most {max_chars} characters, describing what the image shows so a screen-reader user gets the same information. If the image contains readable text, include the key words. Plain text only, no quotes, no preamble.{title}", ], 'system_suffix' => [ 'label' => __('Output rules (appended to every system instruction)', 'aipcf-ai-provider-for-cloudflare'), 'description' => __('Added to the end of the system instruction of every chat request, including the AI plugin\'s features. Keeps open-weight models from wrapping answers in commentary. Leave empty to disable.', 'aipcf-ai-provider-for-cloudflare'), 'placeholders' => [], 'model' => __('Runs on every chat model.', 'aipcf-ai-provider-for-cloudflare'), 'default' => 'Output rules: reply with the requested output only. No preamble, no explanation, no notes about how the output meets the requirements, no surrounding quotation marks.', ], 'vision_system' => [ 'label' => __('Default system message for image requests', 'aipcf-ai-provider-for-cloudflare'), 'description' => __('Cloudflare\'s vision endpoint requires a system message; this one is used when the caller did not provide any.', 'aipcf-ai-provider-for-cloudflare'), 'placeholders' => [], 'model' => __('Runs on the vision model.', 'aipcf-ai-provider-for-cloudflare'), 'default' => 'You are a helpful assistant that describes images accurately.', ], ]; } public static function overrides(): array { $o = get_option(self::OPTION, []); return is_array($o) ? array_map('strval', $o) : []; } public static function raw(string $key): string { $defs = self::definitions(); if (!isset($defs[$key])) { return ''; } $o = self::overrides(); return array_key_exists($key, $o) ? $o[$key] : $defs[$key]['default']; } public static function isCustom(string $key): bool { $o = self::overrides(); return array_key_exists($key, $o) && $o[$key] !== (self::definitions()[$key]['default'] ?? null); } public static function render(string $key, array $vars = []): string { $vars = $vars + [ 'site_name' => (string) get_bloginfo('name'), 'language' => self::languageName(), 'max_chars' => $key === 'alt_text' ? '125' : '160', 'title' => '', ]; $text = self::raw($key); foreach ($vars as $k => $v) { $text = str_replace('{' . $k . '}', (string) $v, $text); } $text = trim($text); return (string) apply_filters('aipcf_prompt', $text, $key, $vars); } public static function save(array $input): void { $out = self::normalize($input); if ($out === []) { delete_option(self::OPTION); } else { update_option(self::OPTION, $out, false); } } /** * Minimal override set for $input: cleaned text, only where it differs from the default. * Writes nothing, so it is safe as the option's sanitize callback. */ public static function normalize(array $input): array { $defs = self::definitions(); $out = []; foreach ($defs as $key => $def) { if (!array_key_exists($key, $input)) { continue; } $text = (string) $input[$key]; $text = str_replace(["\r\n", "\r"], "\n", $text); $text = trim(wp_kses($text, [])); if (mb_strlen($text) > self::MAX_LENGTH) { $text = mb_substr($text, 0, self::MAX_LENGTH); } if ($text !== $def['default']) { $out[$key] = $text; } } return $out; } public static function languageName(): string { $locale = get_locale(); if (function_exists('wp_get_available_translations')) { // Avoid the HTTP call; use the local list when present. $translations = get_site_transient('available_translations'); if (is_array($translations) && isset($translations[$locale]['english_name'])) { return (string) $translations[$locale]['english_name']; } } $map = ['en' => 'English', 'hi' => 'Hindi', 'mr' => 'Marathi', 'gu' => 'Gujarati', 'bn' => 'Bengali', 'ta' => 'Tamil', 'te' => 'Telugu', 'kn' => 'Kannada', 'ml' => 'Malayalam', 'pa' => 'Punjabi', 'ur' => 'Urdu', 'fr' => 'French', 'de' => 'German', 'es' => 'Spanish', 'pt' => 'Portuguese', 'it' => 'Italian', 'nl' => 'Dutch', 'ja' => 'Japanese', 'zh' => 'Chinese', 'ar' => 'Arabic', 'ru' => 'Russian', 'tr' => 'Turkish', 'id' => 'Indonesian', 'vi' => 'Vietnamese', 'th' => 'Thai']; $code = strtolower(substr($locale, 0, 2)); return $map[$code] ?? 'the site\'s language'; } }