diff --git a/.gitattributes b/.gitattributes index 9ebfaf5..f60804a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,4 @@ /.gitignore export-ignore /build.sh export-ignore /builds/ export-ignore +/.github/ export-ignore diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..27f1ad0 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,38 @@ +name: Lint + +on: + push: + branches: [main] + pull_request: + +jobs: + php: + runs-on: ubuntu-latest + strategy: + matrix: + # Oldest supported (README: PHP 7.0+) and current. + php: ['7.0', '8.4'] + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: none + - name: Syntax check + run: find . -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php -l + + js: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Syntax check + run: for f in admin/js/*.js; do node --check "$f"; done + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build ZIP (checks versions match and bundles the update checker) + run: ./build.sh + - name: Confirm update checker is bundled + run: unzip -l builds/everyalt-*.zip | grep -q 'everyalt/vendor/plugin-update-checker/plugin-update-checker.php' diff --git a/README.md b/README.md index 8adc703..499fd9d 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ # 🖼️ EveryAlt — Alt text for every image. **Zero limits. Totally free.**

- 🆓 100% free · ♾️ No limits from us · 🔑 Your OpenAI key + 🆓 100% free · ♾️ No limits from us · 🔑 Your AI key: OpenAI, Gemini, or DeepInfra

-> **EveryAlt 1.0 is 100% free.** Bring your own [OpenAI API key](https://platform.openai.com/api-keys), and generate as much alt text as you want. We don’t meter you, cap you, or bill you. **Ever.** +> **EveryAlt 1.0 is 100% free.** Bring your own API key from **OpenAI**, **Google Gemini**, or **DeepInfra**, and generate as much alt text as you want. We don’t meter you, cap you, or bill you. **Ever.** --- @@ -28,10 +28,10 @@ No more blank alt fields. No more guessing. **You upload. EveryAlt writes.** | Before | **EveryAlt 1.0** | |--------|------------------| | Limits, tiers, or paywalls | **Unlimited** — your key, your usage | -| Extra service to sign up for | **Just OpenAI** — one key, one place | -| Opaque pricing | **Transparent** — you pay OpenAI only | +| Extra service to sign up for | **Your choice of provider** — one key, no EveryAlt account | +| Opaque pricing | **Transparent** — you pay your AI provider only | -We don’t charge. We don’t sit in the middle. You use **your** OpenAI key. EveryAlt stays **free**, and you have **no limits** from us. +We don’t charge. We don’t sit in the middle. You use **your** own key. EveryAlt stays **free**, and you have **no limits** from us. --- @@ -55,19 +55,61 @@ Works on localhost, behind HTTP auth, and with your existing workflow. 1. **Upload** the plugin to `wp-content/plugins/everyalt` (or install via WordPress admin). 2. **Activate** the plugin (Plugins → EveryAlt → Activate). -3. **Open Media → EveryAlt** and add your [OpenAI API key](https://platform.openai.com/api-keys) in **Settings**. +3. **Open Media → EveryAlt → Settings**, pick an **AI model**, and paste the API key for that provider (see [Models & keys](#-models--keys)). 4. Turn on **Auto-generate on upload** if you want, or use **Bulk** / **Review** to handle existing images. That’s it. No account with us. No caps. No “upgrade to unlock more.” --- -## 🔑 Your key, your usage +## 🤖 Models & keys -- Alt text is generated by **OpenAI** (image is sent as base64; works on localhost and behind HTTP auth). -- Your API key is **stored encrypted** in your WordPress database. -- **You** are billed by OpenAI for your usage; EveryAlt does not charge you. -- We use a small, efficient model so cost per image stays low. +Choose a model in **Settings**. Every model reads the image itself and writes the alt text (and titles). You only need a key for the provider you pick, and you can store keys for several providers and switch any time. + +| Model | Provider | Input / 1M tokens | Output / 1M tokens | +|-------|----------|------------------:|-------------------:| +| **GPT-5.4 nano** (default) | OpenAI | $0.20 | $1.25 | +| **Gemini 3.1 Flash-Lite** | Google Gemini | $0.25 | $1.50 | +| **DeepSeek V4.1 Flash** | DeepInfra | $0.20 | $0.60 | +| **GLM-5.3-Flash** | DeepInfra | $0.15 | $0.50 | + +Prices are each provider’s published regular rates as of September 2026 and can change: [OpenAI pricing](https://openai.com/api/pricing/) · [Gemini pricing](https://ai.google.dev/gemini-api/docs/pricing) · [DeepInfra pricing](https://deepinfra.com/pricing). The actual cost of every image is recorded on the **Logs** tab. + +> **Upgrading from 1.0.x?** Earlier versions used `gpt-5-nano`, which OpenAI is retiring. Existing installs switch to **GPT-5.4 nano** automatically. Your OpenAI key keeps working. + +### Getting a key + +**OpenAI** +1. Sign in at [platform.openai.com/api-keys](https://platform.openai.com/api-keys). +2. Click **Create new secret key**, name it (e.g. “EveryAlt”), and copy it. It is only shown once. +3. Add a payment method or prepaid credits under billing; generation fails until the account has credit. + +OpenAI does not train on API data by default ([details](https://openai.com/enterprise-privacy/)). + +**Google Gemini** +1. Sign in to Google AI Studio at [aistudio.google.com/apikey](https://aistudio.google.com/apikey). +2. Click **Create API key**, pick or create a Google Cloud project, and copy the key. +3. Optional: enable billing on the project to use the paid tier. + +Gemini has a free tier, but Google may use free-tier content to improve its products; paid-tier content is not used that way ([Gemini API terms](https://ai.google.dev/gemini-api/terms)). + +**DeepInfra** (one key covers both DeepSeek V4.1 Flash and GLM-5.3-Flash) +1. Sign in or sign up at [deepinfra.com/dash/api_keys](https://deepinfra.com/dash/api_keys). +2. Click **New API key**, name it, and copy it. +3. Add a payment method or credits in your DeepInfra billing settings. + +🔒 **Your images stay private with DeepInfra.** DeepInfra runs these models on its own infrastructure in data centers in the **US and Canada**, with **zero data retention**: images and generated text are processed in memory and not stored, request content is not logged, and your data is never used for training. See [DeepInfra data privacy](https://docs.deepinfra.com/account/data-privacy), the [privacy policy](https://deepinfra.com/privacy), and the [trust center](https://trust.deepinfra.com/) (SOC 2 and ISO 27001). + +### Your key, your usage + +- The image is sent to your chosen provider as base64, so it works on localhost and behind HTTP auth. +- API keys are **stored encrypted** in your WordPress database. +- **You** are billed by your provider for usage; EveryAlt does not charge you. + +### For developers + +- `everyalt_model` filters the model ID sent to the provider: `( string $model_id, string $model_slug )`. This replaces `everyalt_openai_model`. +- `everyalt_input_token_price_per_million` / `everyalt_output_token_price_per_million` override the prices used for cost estimates: `( float $price, string $model_slug )`. --- @@ -75,7 +117,7 @@ That’s it. No account with us. No caps. No “upgrade to unlock more.” - WordPress 5.5+ - PHP 7.0+ -- An [OpenAI API key](https://platform.openai.com/api-keys) with API access +- An API key from [OpenAI](https://platform.openai.com/api-keys), [Google Gemini](https://aistudio.google.com/apikey), or [DeepInfra](https://deepinfra.com/dash/api_keys) --- @@ -99,6 +141,23 @@ The plugin uses the `everyalt` text domain and ships with a `.pot` in `languages ## 📝 Changelog +### Unreleased +- **New: choose your AI model.** Pick from **GPT-5.4 nano** (OpenAI), **Gemini 3.1 Flash-Lite** (Google), or **DeepSeek V4.1 Flash** / **GLM-5.3-Flash** (DeepInfra) in Settings. Keys are stored per provider, so you can switch without re-entering them. Settings shows each model's published pricing, step-by-step key instructions, and privacy details, including DeepInfra's US/Canada hosting and zero data retention. +- **Changed** OpenAI requests now use **GPT-5.4 nano**; `gpt-5-nano` is being retired. Existing installs switch automatically. +- **Added** a Model column to the Logs tab and CSV export. +- **Developer:** the `everyalt_openai_model` filter is replaced by `everyalt_model`; the price filters now also receive the model slug. +- **Fixed** automatic updates: release ZIPs now bundle the update checker library (it was missing from ZIPs built with `build.sh`). +- **Fixed** deactivating the plugin no longer deletes your API key and settings. Uninstalling now removes all EveryAlt data (settings, key, logs) instead of leaving it behind. +- **Fixed** "Auto-generate on upload" now starts checked on fresh installs, as intended. +- **Fixed** Editors and Authors can now use the block-editor and media-screen "Generate alt text" buttons on images they can edit, and failures now show a message instead of doing nothing. +- **Fixed** auto-generation on upload now runs exactly once per image, after all image sizes exist (it could previously retry once per image size after a failure). +- **Fixed** the 4MB limit now applies to the resized image actually sent to OpenAI, not the original upload, so large phone photos are no longer skipped. +- **Improved** the Bulk and Review tabs are paginated and much faster on large media libraries. +- **Improved** block-editor and media-screen button text is now translatable. +- **Added** a warning when the saved API key can no longer be decrypted (e.g. after the security keys in `wp-config.php` change). +- **Security** CSV log exports are protected against spreadsheet formula injection; the unused HTTP-auth username/password settings (the password was stored in plain text) have been removed and are deleted on update. +- **Removed** the unused `every_alt_logs` database table (dropped on uninstall) and the legacy `/get_tokens` REST endpoint. + ### 1.0.2 - **New — AI image titles.** EveryAlt now writes descriptive WordPress image **titles**, not just alt text: - Optional **Automatically generate image titles when images are uploaded** toggle in Settings. diff --git a/admin/class-everyalt-admin.php b/admin/class-everyalt-admin.php index 469bcdd..d5bb320 100644 --- a/admin/class-everyalt-admin.php +++ b/admin/class-everyalt-admin.php @@ -44,7 +44,7 @@ class Every_Alt_Admin { * @param string $message Short message * @param string $detail Optional longer detail (e.g. API error body) * @param string $usage Optional token usage (e.g. "Input Tokens: 193, Output Tokens: 12, Total: 205") - * @param string $cost Optional estimated cost in cents (e.g. "0.0123¢" for gpt-5-nano) + * @param string $cost Optional estimated cost in cents (e.g. "0.0123¢") */ private function every_alt_add_generation_log( $attachment_id, $status, $message, $detail = '', $usage = '', $cost = '' ) { $log = get_option( self::GENERATION_LOG_OPTION, array() ); @@ -56,6 +56,7 @@ private function every_alt_add_generation_log( $attachment_id, $status, $message 'detail' => $detail, 'usage' => $usage, 'cost' => $cost, + 'model' => $this->every_alt_selected_model_label(), ); array_unshift( $log, $entry ); $log = array_slice( $log, 0, self::GENERATION_LOG_MAX ); @@ -112,6 +113,7 @@ public function every_alt_export_logs_csv() { __( 'Time', 'everyalt' ), __( 'Attachment ID', 'everyalt' ), __( 'Status', 'everyalt' ), + __( 'Model', 'everyalt' ), __( 'Message / Alt text', 'everyalt' ), __( 'Cost (USD)', 'everyalt' ), __( 'Details', 'everyalt' ), @@ -134,48 +136,127 @@ public function every_alt_export_logs_csv() { isset( $entry['time'] ) ? $entry['time'] : '', isset( $entry['attachment_id'] ) ? $entry['attachment_id'] : '', $status_label, + isset( $entry['model'] ) ? $entry['model'] : '', isset( $entry['message'] ) ? $entry['message'] : '', $cost_usd, $detail, ); - fputcsv( $out, $row ); + fputcsv( $out, array_map( array( $this, 'every_alt_csv_safe_cell' ), $row ) ); } fclose( $out ); exit; } /** - * AJAX: Validate OpenAI API key (key from POST or stored key if empty). + * Neutralize spreadsheet formulas: model output or API errors starting with =, +, -, @ (or a tab/CR) + * would otherwise be evaluated when the CSV is opened in Excel or Sheets. + * + * @param mixed $value + * @return mixed + */ + private function every_alt_csv_safe_cell( $value ) { + if ( is_string( $value ) && $value !== '' && strpos( "=+-@\t\r", $value[0] ) !== false ) { + return "'" . $value; + } + return $value; + } + + /** + * One-time cleanup when the plugin version changes (updates do not re-run the activation hook). + */ + public function every_alt_maybe_upgrade() { + if ( get_option( 'every_alt_version' ) === $this->version ) { + return; + } + // Settings from earlier versions that are no longer used. The HTTP auth password was stored in plain text. + delete_option( 'every_alt_secret' ); + delete_option( 'every_alt_fulltext' ); + delete_option( 'every_alt_httpuser' ); + delete_option( 'every_alt_httpassword' ); + // Sites that already have a key have already chosen their auto-generate setting. + foreach ( Every_Alt_Providers::providers() as $provider ) { + if ( get_option( $provider['key_option'], '' ) !== '' ) { + delete_option( 'every_alt_do_auto_default' ); + break; + } + } + // gpt-5-nano is being retired; installs from before model selection move to GPT-5.4 nano. + add_option( Every_Alt_Providers::MODEL_OPTION, Every_Alt_Providers::DEFAULT_MODEL ); + update_option( 'every_alt_version', $this->version ); + } + + /** + * Warn admins when a key is stored but can no longer be decrypted (e.g. AUTH_KEY in wp-config.php changed). + */ + public function every_alt_key_decrypt_notice() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + $unreadable = array(); + foreach ( Every_Alt_Providers::providers() as $slug => $provider ) { + if ( get_option( $provider['key_option'], '' ) !== '' && Every_Alt_Providers::get_key( $slug ) === '' ) { + $unreadable[] = $provider['label']; + } + } + if ( ! $unreadable ) { + return; + } + $url = admin_url( 'upload.php?page=everyalt&tab=settings' ); + echo '

' . wp_kses( + sprintf( + /* translators: 1: provider name(s), e.g. "OpenAI", 2: URL of the EveryAlt settings tab */ + __( 'EveryAlt can no longer read your saved %1$s API key, usually because the security keys in wp-config.php changed. Generation with that provider is paused until you re-enter the key.', 'everyalt' ), + esc_html( implode( ', ', $unreadable ) ), + esc_url( $url ) + ), + array( 'a' => array( 'href' => true ) ) + ) . '

'; + } + + /** + * AJAX: Validate a provider's API key (key from POST, or that provider's stored key if empty). */ public function ajax_validate_key() { check_ajax_referer( 'everyalt_validate_key', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to do this.', 'everyalt' ) ) ); } + $provider = isset( $_POST['provider'] ) ? sanitize_key( wp_unslash( $_POST['provider'] ) ) : 'openai'; + if ( ! Every_Alt_Providers::provider( $provider ) ) { + wp_send_json_error( array( 'message' => __( 'Unknown provider.', 'everyalt' ) ) ); + } $key = isset( $_POST['key'] ) ? sanitize_text_field( wp_unslash( $_POST['key'] ) ) : ''; if ( $key === '' ) { - $key = $this->get_openai_key(); + $key = Every_Alt_Providers::get_key( $provider ); } if ( $key === '' ) { wp_send_json_error( array( 'message' => __( 'Enter an API key in the field above, or save a key first to validate the stored key.', 'everyalt' ) ) ); } - if ( Every_Alt_OpenAI::validate_api_key( $key ) ) { + if ( Every_Alt_Providers::validate_key( $provider, $key ) ) { wp_send_json_success( array( 'message' => __( 'API key is valid.', 'everyalt' ) ) ); } wp_send_json_error( array( 'message' => __( 'API key could not be validated. Check that the key is correct and has API access.', 'everyalt' ) ) ); } /** - * Get decrypted OpenAI API key (stored encrypted in DB). + * Decrypted API key for the selected model's provider ('' if none is saved). * * @return string */ - private function get_openai_key() { - $encrypted = get_option( Every_Alt_Encryption::OPTION_KEY, '' ); - if ( $encrypted === '' ) { - return ''; - } - return Every_Alt_Encryption::decrypt( $encrypted ); + private function get_api_key() { + $model = Every_Alt_Providers::selected_model(); + return Every_Alt_Providers::get_key( $model['provider'] ); + } + + /** + * Display name of the selected model, e.g. "GPT-5.4 nano (OpenAI)". + * + * @return string + */ + private function every_alt_selected_model_label() { + $model = Every_Alt_Providers::selected_model(); + $provider = Every_Alt_Providers::provider( $model['provider'] ); + return $model['label'] . ' (' . $provider['label'] . ')'; } @@ -199,9 +280,6 @@ private function get_openai_key() { */ private $version; - - public $error; - /** * The hook suffix for the plugin's options page. * @@ -222,7 +300,6 @@ public function __construct( $plugin_name, $version ) { $this->plugin_name = $plugin_name; $this->version = $version; - $this->error = false; } @@ -270,10 +347,36 @@ public function enqueue_scripts() { 'restNonce' => wp_create_nonce( 'wp_rest' ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'validateKeyNonce' => wp_create_nonce( 'everyalt_validate_key' ), + 'i18n' => array( + 'error' => __( 'Error', 'everyalt' ), + 'errorPrefix' => __( 'Error:', 'everyalt' ), + 'requestFailed' => __( 'Request failed', 'everyalt' ), + 'saved' => __( 'Saved!', 'everyalt' ), + 'saveFailed' => __( 'Save failed', 'everyalt' ), + 'regenerated' => __( 'Regenerated!', 'everyalt' ), + 'regenerateFailed' => __( 'Regenerate failed', 'everyalt' ), + ), ) ); } } + /** + * UI strings shared by the media-screen and block-editor "Generate alt text" buttons. + * + * @return array + */ + private function every_alt_button_i18n() { + return array( + 'panelTitle' => __( 'EveryAlt', 'everyalt' ), + 'button' => __( 'Generate alt text with EveryAlt', 'everyalt' ), + 'selectImage' => __( 'Select or upload an image to generate alt text.', 'everyalt' ), + 'generating' => __( 'Generating…', 'everyalt' ), + 'generated' => __( 'Alt text generated.', 'everyalt' ), + 'failed' => __( 'Could not generate alt text.', 'everyalt' ), + 'errorPrefix' => __( 'Error:', 'everyalt' ), + ); + } + //redirect after activation /** @@ -287,10 +390,13 @@ public function add_custom_button_to_image_block() { wp_enqueue_script( $this->plugin_name . '-gutenberg-image', plugin_dir_url( __FILE__ ) . 'js/everyalt-gutenberg-button.js', - array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-hooks', 'wp-api-fetch' ), + array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-hooks', 'wp-api-fetch', 'wp-data', 'wp-notices' ), $this->every_alt_asset_version( 'js/everyalt-gutenberg-button.js' ), true ); + wp_localize_script( $this->plugin_name . '-gutenberg-image', 'everyaltBlock', array( + 'i18n' => $this->every_alt_button_i18n(), + ) ); } @@ -328,43 +434,45 @@ public function every_alt_custom_button_to_media_edit_page() { 'restUrl' => rest_url(), 'restNonce' => wp_create_nonce( 'wp_rest' ), 'mediaId' => (int) $post->ID, + 'i18n' => $this->every_alt_button_i18n(), ) ); include_once 'partials/everyalt-custom-media-button.php'; } /** - * When attachment metadata is updated (after upload, so medium size exists), run auto alt if enabled and alt is empty. + * Filter: wp_generate_attachment_metadata. Runs once per upload, after every sub-size has been + * written to disk, so the medium size is available. Runs auto alt/title if enabled. + * + * The metadata has not been saved yet at this point, so it is passed through to the generator. * - * @param int $meta_id - * @param int $object_id - * @param string $meta_key - * @param mixed $meta_value + * @param array $metadata + * @param int $attachment_id + * @return array Unchanged metadata. */ - public function every_alt_maybe_auto_after_metadata( $meta_id, $object_id, $meta_key, $meta_value ) { - if ( $meta_key !== '_wp_attachment_metadata' || ! wp_attachment_is_image( $object_id ) ) { - return; + public function every_alt_maybe_auto_on_generate_metadata( $metadata, $attachment_id ) { + if ( ! is_array( $metadata ) || ! wp_attachment_is_image( $attachment_id ) ) { + return $metadata; } - if ( ! is_array( $meta_value ) || empty( $meta_value['sizes'] ) ) { - return; - } - if ( ! $this->get_openai_key() ) { - return; + if ( ! $this->get_api_key() ) { + return $metadata; } // Auto alt text: only when alt is currently empty. - if ( get_option( $this->option_name . '_auto' ) && get_post_meta( $object_id, '_wp_attachment_image_alt', true ) === '' ) { - $this->every_alt_auto_add_image_alt_text( $object_id, false ); + if ( get_option( $this->option_name . '_auto' ) && get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) === '' ) { + $this->every_alt_auto_add_image_alt_text( $attachment_id, false, $metadata ); } // Auto title: only when the current title still looks like the raw filename. - if ( get_option( $this->option_name . '_auto_title' ) && $this->every_alt_title_is_filename_like( $object_id ) ) { - $this->every_alt_auto_add_image_title( $object_id, false ); + if ( get_option( $this->option_name . '_auto_title' ) && $this->every_alt_title_is_filename_like( $attachment_id ) ) { + $this->every_alt_auto_add_image_title( $attachment_id, false, $metadata ); } + + return $metadata; } //auto alt – direct OpenAI Vision API, image sent as base64 (medium size) - public function every_alt_auto_add_image_alt_text( $attachment_ID, $bulk = null ) { - $api_key = $this->get_openai_key(); + public function every_alt_auto_add_image_alt_text( $attachment_ID, $bulk = null, $metadata = null ) { + $api_key = $this->get_api_key(); $auto = get_option( $this->option_name . '_auto' ); if ( $bulk ) { $auto = 1; @@ -377,13 +485,13 @@ public function every_alt_auto_add_image_alt_text( $attachment_ID, $bulk = null $this->every_alt_add_generation_log( $attachment_ID, 'error', __( 'Skipped: API key validation failed.', 'everyalt' ), '' ); return null; } - if ( ! $this->every_alt_is_valid_image( $attachment_ID ) ) { + if ( ! $this->every_alt_is_valid_image( $attachment_ID, $metadata ) ) { $this->every_alt_add_generation_log( $attachment_ID, 'error', __( 'Skipped: not a valid image or file exceeds 4MB.', 'everyalt' ), '' ); return null; } $openai = new Every_Alt_OpenAI( $api_key ); - $generated_alt = $openai->generate_alt( $attachment_ID ); + $generated_alt = $openai->generate_alt( $attachment_ID, $metadata ); if ( ! empty( $generated_alt->error ) ) { $usage = isset( $generated_alt->usage ) ? $generated_alt->usage : ''; @@ -404,7 +512,6 @@ public function every_alt_auto_add_image_alt_text( $attachment_ID, $bulk = null } update_post_meta( $attachment_ID, '_wp_attachment_image_alt', $generated_alt->alt ); - $this->every_alt_media_logs( $generated_alt->alt, $attachment_ID ); $usage = isset( $generated_alt->usage ) ? $generated_alt->usage : ''; $cost = isset( $generated_alt->cost ) ? $generated_alt->cost : ''; $this->every_alt_add_generation_log( $attachment_ID, 'success', $generated_alt->alt, '', $usage, $cost ); @@ -416,8 +523,8 @@ public function every_alt_auto_add_image_alt_text( $attachment_ID, $bulk = null } //auto title – direct OpenAI Vision API, image sent as base64 (medium size) - public function every_alt_auto_add_image_title( $attachment_ID, $bulk = null ) { - $api_key = $this->get_openai_key(); + public function every_alt_auto_add_image_title( $attachment_ID, $bulk = null, $metadata = null ) { + $api_key = $this->get_api_key(); $auto = get_option( $this->option_name . '_auto_title' ); if ( $bulk ) { $auto = 1; @@ -430,13 +537,13 @@ public function every_alt_auto_add_image_title( $attachment_ID, $bulk = null ) { $this->every_alt_add_generation_log( $attachment_ID, 'error', __( '[Title] Skipped: API key validation failed.', 'everyalt' ), '' ); return null; } - if ( ! $this->every_alt_is_valid_image( $attachment_ID ) ) { + if ( ! $this->every_alt_is_valid_image( $attachment_ID, $metadata ) ) { $this->every_alt_add_generation_log( $attachment_ID, 'error', __( '[Title] Skipped: not a valid image or file exceeds 4MB.', 'everyalt' ), '' ); return null; } $openai = new Every_Alt_OpenAI( $api_key ); - $generated_title = $openai->generate_title( $attachment_ID ); + $generated_title = $openai->generate_title( $attachment_ID, $metadata ); if ( ! empty( $generated_title->error ) ) { $usage = isset( $generated_title->usage ) ? $generated_title->usage : ''; @@ -482,7 +589,18 @@ private function every_alt_title_is_filename_like( $attachment_id ) { if ( ! $post ) { return false; } - $title = trim( (string) $post->post_title ); + return $this->every_alt_title_looks_like_filename( $post->post_title, get_attached_file( $attachment_id ) ); + } + + /** + * Core of every_alt_title_is_filename_like(), working on raw values so it can run over query rows. + * + * @param string $title Attachment post_title. + * @param string|false $file Attached file path (or the relative _wp_attached_file value); only its basename is used. + * @return bool + */ + private function every_alt_title_looks_like_filename( $title, $file ) { + $title = trim( (string) $title ); if ( $title === '' ) { return true; } @@ -501,7 +619,6 @@ private function every_alt_title_is_filename_like( $attachment_id ) { }; // Primary signal: title matches the uploaded filename (without extension), ignoring punctuation. - $file = get_attached_file( $attachment_id ); if ( $file ) { $base = pathinfo( $file, PATHINFO_FILENAME ); if ( $normalize( $work ) === $normalize( $base ) ) { @@ -583,213 +700,153 @@ private function every_alt_token_looks_random( $token ) { return $transitions >= 4; } - private function every_alt_is_valid_image($media_id) { - $attachment = get_post($media_id); - if (wp_attachment_is_image($attachment)) { - $file_path = get_attached_file($media_id); - $file_size = filesize($file_path); - $max_size = 4 * 1024 * 1024; // 4MB in bytes - if ($file_size <= $max_size) { - return true; - } + /** + * Whether an attachment can be sent for generation: an image whose file we would actually send + * (medium size when available, see Every_Alt_OpenAI::get_image_path_for_vision()) is at most 4MB. + * + * @param int $media_id + * @param array|null $metadata Attachment metadata, when not yet saved (during upload). + * @return bool + */ + private function every_alt_is_valid_image( $media_id, $metadata = null ) { + if ( ! wp_attachment_is_image( $media_id ) ) { + return false; } - return false; - } - - private function every_alt_media_logs($alt,$media_id){ - $date = new DateTime(); - global $wpdb; - $table_name = $wpdb->prefix . 'every_alt_logs'; - - $media = $wpdb->get_row( - $wpdb->prepare( - "SELECT * FROM $table_name WHERE media_id = %d ORDER BY id ASC LIMIT 1", - $media_id - ) - ); - if ($media) { - // Update alt text - $wpdb->update( - $table_name, - array('alt_text' => sanitize_text_field($alt)), - array('id' => $media->id) - ); - }else{ - $wpdb->insert( - $table_name, - array( - 'media_id' => $media_id, - 'alt_text' => sanitize_text_field($alt), - 'created' => $date->format('Y-m-d H:i:s') - ), - array( - '%s', - '%s', - '%s' - ) - ); - } - - - } - - - public function every_alt_on_media_delete($post_id) { - global $wpdb; - $table_name = $wpdb->prefix . 'every_alt_logs'; - $media = $wpdb->get_row( - $wpdb->prepare( - "SELECT * FROM $table_name WHERE media_id = %d ORDER BY id ASC LIMIT 1", - $post_id - ) - ); - if ($media) { - $wpdb->delete($table_name, array('id' => $media->id)); - } - return; + $path = Every_Alt_OpenAI::get_image_path_for_vision( $media_id, $metadata ); + if ( ! $path ) { + return false; + } + $file_size = filesize( $path ); + return $file_size !== false && $file_size <= 4 * 1024 * 1024; } private function every_alt_validate_token() { - $api_key = $this->get_openai_key(); + $api_key = $this->get_api_key(); return ! empty( $api_key ); } + /** Images shown per page on the Bulk and Review tabs. */ + const IMAGES_PER_PAGE = 60; - - private function every_alt_get_images(){ - global $wpdb; - // Set the current page number - $page_number = isset( $_REQUEST['paged'] ) ? max( 1, absint( $_REQUEST['paged'] ) ) : 1; - - $per_page = 35; - $offset = ( $page_number - 1 ) * $per_page; - $total_items = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}every_alt_logs" ); - $results = $wpdb->get_results( - $wpdb->prepare( - "SELECT * FROM {$wpdb->prefix}every_alt_logs ORDER BY id DESC LIMIT %d OFFSET %d", - $per_page, - $offset - ) - ); - - if(!$results){ - $response = [ - 'images' => [], - 'pagination' => false, - ]; - return $response; - } - $pagination = paginate_links(array( - 'base' => add_query_arg('paged', '%#%'), - 'format' => '', - 'current' => $page_number, - 'total' => ceil($total_items / $per_page), - )); - - //get all the needed data - $images = []; - foreach ($results as $image) { - $array = [ - 'id'=>$image->id, - 'media_id'=>$image->media_id, - 'media_link'=>get_edit_post_link($image->media_id), - 'alt_text'=> get_post_meta($image->media_id, '_wp_attachment_image_alt', true), - 'image_url'=>wp_get_attachment_image_url($image->media_id) - ]; - $images[] = $array; - - - } - - $response = [ - 'images' => $images, - 'pagination' => $pagination, - ]; - - return $response; - - } - - - private function every_alt_get_images_without_alt(){ - $images = get_posts( array( - 'post_type' => 'attachment', - 'post_mime_type' => 'image', - 'posts_per_page' => -1, - 'post_status' => 'any', - ) ); - $images_without_alt = array(); - foreach ( $images as $image ) { - $alt = get_post_meta( $image->ID, '_wp_attachment_image_alt', true ); - if ( empty( $alt ) ) { - $images_without_alt[] = $image; - } - } - return $images_without_alt; + /** + * Current page number for the Bulk and Review tabs. + * + * @return int + */ + private function every_alt_current_page() { + return isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1; } /** - * Get image attachments that already have alt text. + * One page of image attachments with or without alt text, newest first. * - * @return array Array of WP_Post objects. + * @param bool $with_alt True for images that have alt text, false for those missing it. + * @return array { images: WP_Post[], total: int, pages: int, current: int } */ - private function every_alt_get_images_with_alt() { - $images = get_posts( array( - 'post_type' => 'attachment', - 'post_mime_type' => 'image', - 'posts_per_page' => -1, - 'post_status' => 'any', - ) ); - $with_alt = array(); - foreach ( $images as $image ) { - $alt = get_post_meta( $image->ID, '_wp_attachment_image_alt', true ); - if ( $alt !== '' ) { - $with_alt[] = $image; - } + private function every_alt_get_images_by_alt( $with_alt ) { + if ( $with_alt ) { + $meta_query = array( + array( + 'key' => '_wp_attachment_image_alt', + 'value' => '', + 'compare' => '!=', + ), + ); + } else { + $meta_query = array( + 'relation' => 'OR', + array( + 'key' => '_wp_attachment_image_alt', + 'compare' => 'NOT EXISTS', + ), + array( + 'key' => '_wp_attachment_image_alt', + 'value' => '', + ), + ); } - return $with_alt; + $current = $this->every_alt_current_page(); + $query = new WP_Query( array( + 'post_type' => 'attachment', + 'post_mime_type' => 'image', + 'post_status' => 'any', + 'posts_per_page' => self::IMAGES_PER_PAGE, + 'paged' => $current, + 'meta_query' => $meta_query, // phpcs:ignore WordPress.DB.SlowDBQuery + 'update_post_term_cache' => false, + ) ); + return array( + 'images' => $query->posts, + 'total' => (int) $query->found_posts, + 'pages' => (int) $query->max_num_pages, + 'current' => $current, + ); } /** - * Get image attachments whose title still looks like the raw filename (i.e. need a title). + * One page of image attachments whose title does (or does not) look like the raw upload filename. + * + * The filename check can't be expressed in SQL, so this scans a lightweight (ID, title, file) row per + * image in one query instead of loading every attachment post. * - * @return array Array of WP_Post objects. + * @param bool $filename_like True for images needing a title, false for images with a custom title. + * @return array { images: WP_Post[], total: int, pages: int, current: int } */ - private function every_alt_get_images_without_title() { - $images = get_posts( array( - 'post_type' => 'attachment', - 'post_mime_type' => 'image', - 'posts_per_page' => -1, - 'post_status' => 'any', - ) ); - $without_title = array(); - foreach ( $images as $image ) { - if ( $this->every_alt_title_is_filename_like( $image->ID ) ) { - $without_title[] = $image; + private function every_alt_get_images_by_title( $filename_like ) { + global $wpdb; + $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + "SELECT p.ID, p.post_title, pm.meta_value AS file + FROM {$wpdb->posts} p + LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_wp_attached_file' + WHERE p.post_type = 'attachment' + AND p.post_mime_type LIKE 'image/%' + AND p.post_status NOT IN ( 'trash', 'auto-draft' ) + ORDER BY p.post_date DESC, p.ID DESC" + ); + $ids = array(); + foreach ( $rows as $row ) { + if ( $this->every_alt_title_looks_like_filename( $row->post_title, $row->file ) === $filename_like ) { + $ids[] = (int) $row->ID; } } - return $without_title; + + $total = count( $ids ); + $pages = (int) ceil( $total / self::IMAGES_PER_PAGE ); + $current = min( $this->every_alt_current_page(), max( 1, $pages ) ); + $page_ids = array_slice( $ids, ( $current - 1 ) * self::IMAGES_PER_PAGE, self::IMAGES_PER_PAGE ); + $images = $page_ids ? get_posts( array( + 'post_type' => 'attachment', + 'post_status' => 'any', + 'post__in' => $page_ids, + 'orderby' => 'post__in', + 'posts_per_page' => count( $page_ids ), + ) ) : array(); + + return array( + 'images' => $images, + 'total' => $total, + 'pages' => $pages, + 'current' => $current, + ); } /** - * Get image attachments that already have a custom (non-filename) title. + * Pagination links for a Bulk/Review tab result from every_alt_get_images_by_*(). * - * @return array Array of WP_Post objects. + * @param array $result + * @param string $tab + * @return string HTML, empty when there is only one page. */ - private function every_alt_get_images_with_title() { - $images = get_posts( array( - 'post_type' => 'attachment', - 'post_mime_type' => 'image', - 'posts_per_page' => -1, - 'post_status' => 'any', - ) ); - $with_title = array(); - foreach ( $images as $image ) { - if ( ! $this->every_alt_title_is_filename_like( $image->ID ) ) { - $with_title[] = $image; - } + private function every_alt_pagination_links( $result, $tab ) { + if ( $result['pages'] < 2 ) { + return ''; } - return $with_title; + return (string) paginate_links( array( + 'base' => add_query_arg( array( 'page' => $this->plugin_name, 'tab' => $tab, 'paged' => '%#%' ), admin_url( 'upload.php' ) ), + 'format' => '', + 'current' => $result['current'], + 'total' => $result['pages'], + ) ); } @@ -842,22 +899,17 @@ public function display_options_page() { $active = $tab; - $has_openai_key = ! empty( $this->get_openai_key() ); - - if ( $active === 'bulk' ) { - $images_without_alt = $this->every_alt_get_images_without_alt(); - } - - if ( $active === 'review' ) { - $images_with_alt = $this->every_alt_get_images_with_alt(); - } + $has_api_key = ! empty( $this->get_api_key() ); - if ( $active === 'bulk_title' ) { - $images_without_title = $this->every_alt_get_images_without_title(); + $image_page = null; + $pagination_links = ''; + if ( $active === 'bulk' || $active === 'review' ) { + $image_page = $this->every_alt_get_images_by_alt( $active === 'review' ); + } elseif ( $active === 'bulk_title' || $active === 'review_title' ) { + $image_page = $this->every_alt_get_images_by_title( $active === 'bulk_title' ); } - - if ( $active === 'review_title' ) { - $images_with_title = $this->every_alt_get_images_with_title(); + if ( $image_page ) { + $pagination_links = $this->every_alt_pagination_links( $image_page, $active ); } if ( $active === 'logs' ) { @@ -873,7 +925,7 @@ public function display_options_page() { public function register_setting() { - // OpenAI key is stored encrypted via Every_Alt_Encryption::OPTION_KEY, not registered here. + // API keys are stored encrypted (see Every_Alt_Providers::save_key()), not registered here. register_setting( $this->plugin_name, $this->option_name . '_auto', @@ -884,16 +936,6 @@ public function register_setting() { ) ); - register_setting( - $this->plugin_name, - $this->option_name . '_fulltext', - array( - 'type' => 'boolean', - 'show_in_rest' => true, - 'default' => false, - ) - ); - register_setting( $this->plugin_name, $this->option_name . '_auto_title', @@ -905,25 +947,6 @@ public function register_setting() { ); - register_setting( - $this->plugin_name, - $this->option_name . '_httpuser', - array( - 'type' => 'string', - 'show_in_rest' => true, - 'default' => '', - ) - ); - - register_setting( - $this->plugin_name, - $this->option_name . '_httpassword', - array( - 'type' => 'string', - 'show_in_rest' => true, - 'default' => '', - ) - ); } /** @@ -936,19 +959,31 @@ public function every_alt_save_settings() { if ( ! current_user_can( 'manage_options' ) ) { return; } - $openai_key = isset( $_POST['every_alt_openai_key'] ) ? sanitize_text_field( wp_unslash( $_POST['every_alt_openai_key'] ) ) : ''; - if ( $openai_key !== '' ) { - if ( ! Every_Alt_OpenAI::validate_api_key( $openai_key ) ) { - wp_safe_redirect( add_query_arg( array( 'page' => 'everyalt', 'tab' => 'settings', 'error' => 'everyalt_invalid_key' ), admin_url( 'upload.php' ) ) ); + // Validate every newly entered key before saving anything, so a typo doesn't half-save the form. + $new_keys = array(); + foreach ( Every_Alt_Providers::providers() as $slug => $provider ) { + $field = $provider['key_option']; + $key = isset( $_POST[ $field ] ) ? sanitize_text_field( wp_unslash( $_POST[ $field ] ) ) : ''; + if ( $key === '' ) { + continue; + } + if ( ! Every_Alt_Providers::validate_key( $slug, $key ) ) { + wp_safe_redirect( add_query_arg( array( 'page' => 'everyalt', 'tab' => 'settings', 'error' => 'everyalt_invalid_key', 'provider' => $slug ), admin_url( 'upload.php' ) ) ); exit; } - $encrypted = Every_Alt_Encryption::encrypt( $openai_key ); - if ( $encrypted !== '' ) { - update_option( Every_Alt_Encryption::OPTION_KEY, $encrypted ); + $new_keys[ $slug ] = $key; + } + foreach ( $new_keys as $slug => $key ) { + Every_Alt_Providers::save_key( $slug, $key ); + } + if ( isset( $_POST[ Every_Alt_Providers::MODEL_OPTION ] ) ) { + $model = sanitize_text_field( wp_unslash( $_POST[ Every_Alt_Providers::MODEL_OPTION ] ) ); + $models = Every_Alt_Providers::models(); + if ( isset( $models[ $model ] ) ) { + update_option( Every_Alt_Providers::MODEL_OPTION, $model ); } } update_option( $this->option_name . '_auto', ! empty( $_POST[ $this->option_name . '_auto' ] ) ? 1 : 0 ); - update_option( $this->option_name . '_fulltext', ! empty( $_POST[ $this->option_name . '_fulltext' ] ) ? 1 : 0 ); update_option( $this->option_name . '_auto_title', ! empty( $_POST[ $this->option_name . '_auto_title' ] ) ? 1 : 0 ); if ( isset( $_POST['every_alt_vision_prompt'] ) ) { update_option( 'every_alt_vision_prompt', sanitize_textarea_field( wp_unslash( $_POST['every_alt_vision_prompt'] ) ) ); @@ -960,32 +995,17 @@ public function every_alt_save_settings() { $val = sanitize_text_field( wp_unslash( $_POST['every_alt_max_completion_tokens'] ) ); update_option( 'every_alt_max_completion_tokens', $val === '' ? '' : max( 1, (int) $val ) ); } - if ( isset( $_POST[ $this->option_name . '_httpuser' ] ) ) { - update_option( $this->option_name . '_httpuser', sanitize_text_field( wp_unslash( $_POST[ $this->option_name . '_httpuser' ] ) ) ); - } - if ( isset( $_POST[ $this->option_name . '_httpassword' ] ) ) { - update_option( $this->option_name . '_httpassword', sanitize_text_field( wp_unslash( $_POST[ $this->option_name . '_httpassword' ] ) ) ); - } + // The form showed the auto-generate default (checked) until now; from here on the saved value applies. + delete_option( 'every_alt_do_auto_default' ); wp_safe_redirect( add_query_arg( array( 'page' => 'everyalt', 'tab' => 'settings', 'updated' => '1' ), admin_url( 'upload.php' ) ) ); exit; } public function every_alt_custom_admin_endpoints() { - register_rest_route( 'everyalt-api/v1', '/get_tokens', array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => [$this,'every_alt_get_tokens'], - 'permission_callback' => function () { - return current_user_can( 'manage_options' ); - } - )); - - register_rest_route( 'everyalt-api/v1', '/save_alt', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'every_alt_save_alt' ), - 'permission_callback' => function () { - return current_user_can( 'manage_options' ); - }, + 'permission_callback' => array( $this, 'every_alt_rest_can_edit_media' ), 'args' => array( 'media_id' => array( 'required' => true, @@ -993,12 +1013,6 @@ public function every_alt_custom_admin_endpoints() { 'minimum' => 1, 'sanitize_callback' => 'absint', ), - 'log_id' => array( - 'required' => false, - 'type' => 'integer', - 'minimum' => 0, - 'sanitize_callback' => 'absint', - ), 'alt_text' => array( 'required' => true, 'type' => 'string', @@ -1010,9 +1024,7 @@ public function every_alt_custom_admin_endpoints() { register_rest_route( 'everyalt-api/v1', '/bulk_generate_alt', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'bulk_generate_alt' ), - 'permission_callback' => function () { - return current_user_can( 'manage_options' ); - }, + 'permission_callback' => array( $this, 'every_alt_rest_can_edit_media' ), 'args' => array( 'media_id' => array( 'required' => true, @@ -1026,9 +1038,7 @@ public function every_alt_custom_admin_endpoints() { register_rest_route( 'everyalt-api/v1', '/save_title', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'every_alt_save_title' ), - 'permission_callback' => function () { - return current_user_can( 'manage_options' ); - }, + 'permission_callback' => array( $this, 'every_alt_rest_can_edit_media' ), 'args' => array( 'media_id' => array( 'required' => true, @@ -1047,9 +1057,7 @@ public function every_alt_custom_admin_endpoints() { register_rest_route( 'everyalt-api/v1', '/bulk_generate_title', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'bulk_generate_title' ), - 'permission_callback' => function () { - return current_user_can( 'manage_options' ); - }, + 'permission_callback' => array( $this, 'every_alt_rest_can_edit_media' ), 'args' => array( 'media_id' => array( 'required' => true, @@ -1066,6 +1074,20 @@ public function every_alt_custom_admin_endpoints() { } + /** + * REST permission: the user can edit this image attachment. Lets Editors and Authors use the + * block-editor and media-screen buttons on media they are allowed to edit, not just admins. + * + * @param WP_REST_Request $request + * @return bool + */ + public function every_alt_rest_can_edit_media( $request ) { + $media_id = absint( $request->get_param( 'media_id' ) ); + return $media_id > 0 + && wp_attachment_is_image( $media_id ) + && current_user_can( 'edit_post', $media_id ); + } + public function bulk_generate_alt( $request ) { $media_id = absint( $request->get_param( 'media_id' ) ); $result = $this->every_alt_auto_add_image_alt_text( $media_id, true ); @@ -1131,12 +1153,10 @@ private function every_alt_get_last_log_message_for_attachment( $attachment_id ) public function every_alt_save_alt( $request ) { $media_id = absint( $request->get_param( 'media_id' ) ); - $log_id = absint( $request->get_param( 'log_id' ) ); $alt_text = sanitize_text_field( $request->get_param( 'alt_text' ) ); $update = update_post_meta( $media_id, '_wp_attachment_image_alt', $alt_text ); $response = [ 'alt' => $alt_text, - 'log_id' => $log_id, 'media_id' => $media_id, 'message' => __( 'Alt successfully updated.', 'everyalt' ), ]; @@ -1159,32 +1179,7 @@ public function every_alt_save_title( $request ) { } private function is_user_authorized() { - return ! empty( $this->get_openai_key() ); - } - - - public function every_alt_get_tokens() { - $has_key = ! empty( $this->get_openai_key() ); - if ( get_option( 'every_alt_do_auto_default', false ) ) { - delete_option( 'every_alt_do_auto_default' ); - if ( $has_key ) { - update_option( $this->option_name . '_auto', 1 ); - } - } - $response = array( - 'error' => false, - 'tokens' => null, - 'used_tokens' => null, - 'auto' => (int) get_option( $this->option_name . '_auto', 0 ), - ); - return new WP_REST_Response( $response, 200 ); + return ! empty( $this->get_api_key() ); } - - - - - - - } diff --git a/admin/class-everyalt-openai.php b/admin/class-everyalt-openai.php index e23118e..4d6e7ec 100644 --- a/admin/class-everyalt-openai.php +++ b/admin/class-everyalt-openai.php @@ -1,7 +1,8 @@ 15, - 'headers' => array( - 'Authorization' => 'Bearer ' . trim( $api_key ), - ), - ) - ); - if ( is_wp_error( $response ) ) { - return false; - } - $code = wp_remote_retrieve_response_code( $response ); - return $code >= 200 && $code < 300; + return Every_Alt_Providers::validate_key( 'openai', $api_key ); } - public function __construct( $api_key ) { - $this->api_key = $api_key; - $this->model = apply_filters( self::FILTER_MODEL, 'gpt-5-nano' ); + /** + * @param string $api_key Key for the model's provider. + * @param array|null $model_def Model definition (with 'slug'); defaults to the model selected in Settings. + */ + public function __construct( $api_key, $model_def = null ) { + $this->api_key = $api_key; + $this->model_def = is_array( $model_def ) ? $model_def : Every_Alt_Providers::selected_model(); + $this->provider_def = Every_Alt_Providers::provider( $this->model_def['provider'] ); + $this->model = apply_filters( self::FILTER_MODEL, $this->model_def['model'], $this->model_def['slug'] ); + } + + /** + * Display name of the model in use, e.g. "GPT-5.4 nano (OpenAI)", for logs. + * + * @return string + */ + public function get_model_label() { + return $this->model_def['label'] . ' (' . $this->provider_def['label'] . ')'; } /** * Get the file path for the best available image size (prefer medium to save tokens). * - * @param int $attachment_id + * @param int $attachment_id + * @param array|null $metadata Attachment metadata to use instead of the saved copy (during upload it is not saved yet). * @return string|null Full path or null. */ - private function get_image_path_for_vision( $attachment_id ) { + public static function get_image_path_for_vision( $attachment_id, $metadata = null ) { $file = get_attached_file( $attachment_id ); if ( ! $file || ! is_readable( $file ) ) { return null; } - $meta = wp_get_attachment_metadata( $attachment_id ); + $meta = is_array( $metadata ) ? $metadata : wp_get_attachment_metadata( $attachment_id ); if ( empty( $meta['sizes'] ) ) { return $file; } @@ -119,17 +120,18 @@ private function get_mime_type( $path ) { } /** - * Generate alt text for an attachment using OpenAI Vision. Image is sent as base64. + * Generate alt text for an attachment using the selected vision model. Image is sent as base64. * - * @param int $attachment_id + * @param int $attachment_id + * @param array|null $metadata Unsaved attachment metadata (during upload), see get_image_path_for_vision(). * @return object { alt: string|null, error: string|null, error_detail: string|null } Always returns object; check ->error for failure. */ - public function generate_alt( $attachment_id ) { + public function generate_alt( $attachment_id, $metadata = null ) { $saved_prompt = get_option( 'every_alt_vision_prompt', '' ); $prompt = $saved_prompt !== '' ? $saved_prompt : self::DEFAULT_PROMPT; $prompt = apply_filters( 'everyalt_vision_prompt', $prompt ); - $result = $this->generate_text( $attachment_id, $prompt ); + $result = $this->generate_text( $attachment_id, $prompt, $metadata ); // Map shared 'text' field onto 'alt' for backward compatibility. $result->alt = isset( $result->text ) ? $result->text : null; @@ -138,17 +140,18 @@ public function generate_alt( $attachment_id ) { } /** - * Generate a short image title for an attachment using OpenAI Vision. Image is sent as base64. + * Generate a short image title for an attachment using the selected vision model. Image is sent as base64. * - * @param int $attachment_id + * @param int $attachment_id + * @param array|null $metadata Unsaved attachment metadata (during upload), see get_image_path_for_vision(). * @return object { title: string|null, error: string|null, error_detail: string|null, usage: string, cost: string } Always returns object; check ->error for failure. */ - public function generate_title( $attachment_id ) { + public function generate_title( $attachment_id, $metadata = null ) { $saved_prompt = get_option( 'every_alt_title_prompt', '' ); $prompt = $saved_prompt !== '' ? $saved_prompt : self::DEFAULT_TITLE_PROMPT; $prompt = apply_filters( 'everyalt_title_prompt', $prompt ); - $result = $this->generate_text( $attachment_id, $prompt ); + $result = $this->generate_text( $attachment_id, $prompt, $metadata ); $result->title = isset( $result->text ) ? $result->text : null; unset( $result->text ); @@ -156,14 +159,15 @@ public function generate_title( $attachment_id ) { } /** - * Shared core: send the image plus a text prompt to OpenAI Vision and return the text response. + * Shared core: send the image plus a text prompt to the selected vision model and return the text response. * * @param int $attachment_id - * @param string $prompt Instruction sent with the image. + * @param string $prompt Instruction sent with the image. + * @param array|null $metadata Unsaved attachment metadata (during upload), see get_image_path_for_vision(). * @return object { text: string|null, error: string|null, error_detail: string|null, usage: string, cost: string } */ - private function generate_text( $attachment_id, $prompt ) { - $path = $this->get_image_path_for_vision( $attachment_id ); + private function generate_text( $attachment_id, $prompt, $metadata = null ) { + $path = self::get_image_path_for_vision( $attachment_id, $metadata ); if ( ! $path ) { return (object) array( 'text' => null, 'error' => 'Could not get image path for attachment.', 'error_detail' => '', 'usage' => '', 'cost' => '' ); } @@ -198,10 +202,18 @@ private function generate_text( $attachment_id, $prompt ) { $saved_max = get_option( 'every_alt_max_completion_tokens', '' ); $max_tokens = $saved_max !== '' ? max( 1, (int) $saved_max ) : self::DEFAULT_MAX_COMPLETION_TOKENS; $max_tokens = apply_filters( 'everyalt_max_completion_tokens', $max_tokens ); + if ( ! empty( $this->provider_def['max_tokens_cap'] ) ) { + $max_tokens = min( (int) $max_tokens, (int) $this->provider_def['max_tokens_cap'] ); + } + $image_url = array( 'url' => $data_url ); + if ( ! empty( $this->provider_def['image_detail'] ) ) { + // OpenAI-only hint: low detail keeps image input tokens (and cost) small; plenty for alt text. + $image_url['detail'] = $this->provider_def['image_detail']; + } $body = array( - 'model' => $this->model, - 'max_completion_tokens' => (int) $max_tokens, - 'messages' => array( + 'model' => $this->model, + $this->provider_def['token_param'] => (int) $max_tokens, + 'messages' => array( array( 'role' => 'user', 'content' => array( @@ -211,18 +223,17 @@ private function generate_text( $attachment_id, $prompt ) { ), array( 'type' => 'image_url', - 'image_url' => array( - 'url' => $data_url, - 'detail' => 'low', - ), + 'image_url' => $image_url, ), ), ), ), ); + $body = array_merge( $body, $this->model_def['params'] ); + $response = wp_remote_post( - self::OPENAI_URL, + $this->provider_def['endpoint'], array( 'timeout' => 60, 'headers' => array( @@ -256,7 +267,7 @@ private function generate_text( $attachment_id, $prompt ) { } return (object) array( 'text' => null, - 'error' => 'OpenAI API returned ' . $code, + 'error' => $this->provider_def['label'] . ' API returned ' . $code, 'error_detail' => $detail, 'usage' => $usage, 'cost' => $cost, @@ -293,7 +304,7 @@ private function generate_text( $attachment_id, $prompt ) { if ( $text === '' ) { return (object) array( 'text' => null, - 'error' => 'OpenAI response had no content.', + 'error' => $this->provider_def['label'] . ' response had no content.', 'error_detail' => $body_raw, 'usage' => $usage, 'cost' => $cost, @@ -347,8 +358,8 @@ private function format_cost( $usage ) { if ( $p === 0 && $c === 0 ) { return ''; } - $input_price = (float) apply_filters( self::FILTER_INPUT_PRICE_PER_MILLION, self::DEFAULT_INPUT_PRICE_PER_MILLION ); - $output_price = (float) apply_filters( self::FILTER_OUTPUT_PRICE_PER_MILLION, self::DEFAULT_OUTPUT_PRICE_PER_MILLION ); + $input_price = (float) apply_filters( self::FILTER_INPUT_PRICE_PER_MILLION, $this->model_def['input_price'], $this->model_def['slug'] ); + $output_price = (float) apply_filters( self::FILTER_OUTPUT_PRICE_PER_MILLION, $this->model_def['output_price'], $this->model_def['slug'] ); $cost_dollars = ( $p * $input_price / 1000000 ) + ( $c * $output_price / 1000000 ); $cost_cents = $cost_dollars * 100; return number_format( $cost_cents, 4 ) . '¢'; diff --git a/admin/css/everyalt-admin.css b/admin/css/everyalt-admin.css index 32c3be7..d157b40 100644 --- a/admin/css/everyalt-admin.css +++ b/admin/css/everyalt-admin.css @@ -1 +1 @@ -*{box-sizing:inherit}.everyalt-settings-page{box-sizing:border-box;font-family:Helvetica Neue,Helvetica,Arial,Lucida Grande,sans-serif;font-size:14px;position:relative}#everyalt-history-images-empty{margin-left:auto;margin-right:auto;margin-top:2rem;max-width:1280px;padding:0 15px}#everyalt-history-images{grid-column-gap:10px;grid-row-gap:10px;display:grid;grid-template-columns:repeat(2,1fr);margin-top:2rem;padding-left:15px;padding-right:15px}@media (min-width:768px){#everyalt-history-images{grid-template-columns:repeat(3,1fr)}}@media (min-width:1200px){#everyalt-history-images{grid-template-columns:repeat(4,1fr)}}@media (min-width:1400px){#everyalt-history-images{grid-template-columns:repeat(5,1fr)}}@media (min-width:1800px){#everyalt-history-images{grid-template-columns:repeat(7,1fr)}}#everyalt-history-images img{height:auto;width:100%}#everyalt-history-images a{display:block;margin-bottom:1rem}#everyalt-history .components-snackbar-list__notice-container{display:flex;justify-content:end;padding-right:15px}#everyalt-custom-media-button .components-snackbar-list{bottom:15px;position:fixed;right:15px}#everyalt-custom-media-button .components-snackbar-list__notice-container{display:flex;justify-content:end}#everyalt-bulk-generator{margin-left:auto;margin-right:auto;max-width:1280px;padding:0 15px}#everyalt-bulk-generator .components-card{background-color:#fefefa;margin-top:2rem}#everyalt-bulk-generator .components-card .everyalt-progress-bar{margin-top:1rem}#everyalt-bulk-generator .components-card .everyalt-progress-bar .every-bar-wrapper{background-color:#fff;border:1px solid #b7c9d9;border-radius:3px;height:20px;overflow:hidden;position:relative}#everyalt-bulk-generator .components-card .everyalt-progress-bar .every-bar-bar{background-color:#0750b6;height:100%;left:0;position:absolute;top:0}#everyalt-bulk-generator .components-card .components-button.is-primary{background-color:#0750b6;text-transform:uppercase}#everyalt-bulk-generator table{margin-top:2rem;width:100%}#everyalt-bulk-generator table th{background-color:#00205b;color:#fff;font-size:.75rem;padding:5px;text-align:left;text-transform:uppercase;width:50%}#everyalt-bulk-generator table td{font-size:.75rem;padding:5px}#everyalt-bulk-generator table td.has_error{background-color:crimson;color:#fff}#everyalt-bulk-generator table tr.even{background-color:#e8e8e8}#everyalt-bulk-generator .everyalt-spinner{display:flex;justify-content:center;text-align:center}#everyalt-bulk-generator .everyalt-spinner-container{text-align:center}#everyalt-bulk-generator .completed-message{padding:20px;text-align:center}#everyalt-bulk-generator .completed-message h2{color:#00205b}#everyalt-plugin .components-snackbar-list{bottom:15px;left:0;position:fixed;width:100%}#everyalt-plugin .is-primary{background-color:#0750b6}#everyalt-plugin .every_alt_http_password_holder{margin-top:50px}#everyalt-plugin .every_alt_http_password_holder .components-base-control__help{margin-top:-15px}#everyalt-plugin .every_alt_http_password_holder h2{margin-bottom:5px}#everyalt-plugin .everyalt-main{padding:0 15px}#everyalt-plugin .components-form-toggle.is-checked .components-form-toggle__track{background-color:#0750b6}#everyalt-plugin .components-card{background-color:#fefefa;margin-top:2rem}#everyalt-plugin .components-card .everyalt-progress-bar{margin-top:2rem}#everyalt-plugin .components-card .everyalt-progress-bar .every-bar-wrapper{background-color:#fff;border:1px solid #b7c9d9;border-radius:3px;height:20px;overflow:hidden;position:relative}#everyalt-plugin .components-card .everyalt-progress-bar .every-bar-bar{background-color:#0750b6;height:100%;left:0;position:absolute;top:0}#everyalt-plugin .components-card .components-toggle-control{margin-top:2rem}#everyalt-plugin .components-card .components-base-control .components-base-control__field{align-items:center;display:flex;margin-bottom:10px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field{width:100%}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field{display:block}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field label{font-weight:600;margin-bottom:10px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field input{border:1px solid #b7c9d9;border-radius:3px;box-shadow:none;color:#444;font-size:14px;height:40px;margin:0 0 10px;padding:9px 15px;width:100%}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field input:disabled{background:#f6f6f6;border-color:#eee;color:#bbb}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field input:focus{border-color:#00a0d2;box-shadow:0 0 0 1px #00a0d2;outline:2px solid transparent;outline-offset:-2px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group{align-items:center;display:flex;margin:2px 0 15px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group .is-primary{background-color:#0750b6;margin-right:10px}#everyalt-plugin .components-snackbar-list__notice-container{display:flex;justify-content:end;padding-right:15px}#everyalt-plugin .components-notice{margin:0;width:100%}#everyalt-plugin .components-placeholder{background:#f6f6f6;box-shadow:none}#everyalt-plugin .components-placeholder .components-placeholder__fieldset{display:flex;justify-content:center}#everyalt-plugin .everyalt-main-error{margin-top:2rem}#everyalt-plugin .everyalt-main{margin-left:auto;margin-right:auto;max-width:1280px}#everyalt-plugin .everyalt-main .components-panel__body{background:#fff;border:1px solid #e2e4e7;margin:25px 0}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__body-toggle{border-bottom:1px solid #d6e2ed}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row{margin:0;padding:10px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control .components-base-control__field{align-items:center;display:flex;margin-bottom:10px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field{width:100%}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field{display:block}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field label{font-weight:600;margin-bottom:10px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field input{border:1px solid #b7c9d9;border-radius:3px;box-shadow:none;color:#444;font-size:14px;height:40px;margin:0 0 10px;padding:9px 15px;width:100%}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field input:disabled{background:#f6f6f6;border-color:#eee;color:#bbb}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field input:focus{border-color:#00a0d2;box-shadow:0 0 0 1px #00a0d2;outline:2px solid transparent;outline-offset:-2px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group{align-items:center;display:flex;margin:2px 0 15px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group .is-primary{margin-right:10px}#everyalt-plugin .everyalt-main .everyalt-info{padding:0 10px 10px}#everyalt-plugin .everyalt-main .everyalt-info h2{font-weight:300;margin-bottom:10px}#everyalt-plugin .everyalt-main .everyalt-info p{margin:0 0 1.5em}#everyalt-plugin .everyalt-main .everyalt-info .everyalt-info-button-group{display:flex}#everyalt-plugin .everyalt-main .everyalt-info .everyalt-info-button-group .is-default{font-size:14px;height:auto;padding:2px 20px}#everyalt-plugin .everyalt-main .everyalt-info .everyalt-info-button-group .is-default:first-child{margin-right:15px}#every_alt_pagination{display:flex;font-size:.85rem;font-weight:700;justify-content:center;margin-top:2rem}#every_alt_pagination a.page-numbers{background-color:#0750b6;color:#fff;display:inline-block;margin-right:-4px;min-width:20px;padding:8px;text-align:center;text-decoration:none}#every_alt_pagination a.page-numbers:hover{background-color:#00205b}#every_alt_pagination a.next,#every_alt_pagination a.prev{background-color:transparent;color:#0750b6;padding:0}#every_alt_pagination a.next:hover,#every_alt_pagination a.prev:hover{background-color:transparent;color:#00205b}#every_alt_pagination a.next{margin-left:10px}#every_alt_pagination a.prev{margin-right:10px}#every_alt_pagination span.current{background-color:#00205b;color:#fff;display:inline-block;margin-right:-4px;min-width:20px;padding:8px;text-align:center}#everyalt-main-error{background-color:#fefefa;margin-left:auto;margin-right:auto;margin-top:2rem;max-width:1280px;padding:15px}#everyalt-main-error h2{color:#00205b;text-align:center!important}#everyalt-gutenber-button p{font-size:.75rem}.everyalt-progress.hidden,.hidden{display:none!important}.everyalt-settings-wrap .form-table,.everyalt-bulk-wrap table,.everyalt-history-wrap table{margin-top:1em}.everyalt-log-detail{max-width:480px;vertical-align:top}.everyalt-log-detail-inner{max-height:280px;overflow-y:auto;overflow-x:auto;padding:6px 8px;background:#f6f7f7;border:1px solid #c3c4c7;border-radius:3px;font-size:12px;line-height:1.4;white-space:pre-wrap;word-break:break-word}.everyalt-key-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.everyalt-key-row .regular-text{flex:1;min-width:200px}.everyalt-bulk-actions{margin:1em 0}.everyalt-bulk-actions .button{margin-right:8px}.everyalt-bulk-progress{margin:1em 0;padding:1em;background:#f6f7f7;border:1px solid #c3c4c7;border-radius:3px}.everyalt-bulk-progress-bar{height:22px;background:#fff;border:1px solid #c3c4c7;border-radius:3px;overflow:hidden}.everyalt-bulk-progress-bar span{display:block;height:100%;background:#2271b1;transition:width .2s}.everyalt-bulk-progress-log{margin:.5em 0 0;padding-left:1.5em;max-height:200px;overflow-y:auto;font-size:13px}.everyalt-bulk-progress-log li.success{color:#00a32a}.everyalt-bulk-progress-log li.error{color:#b32d2e}.everyalt-bulk-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px;list-style:none;margin:1em 0;padding:0}.everyalt-bulk-item label{display:block;cursor:pointer}.everyalt-bulk-item .everyalt-bulk-thumb{display:block;line-height:0}.everyalt-bulk-item .everyalt-bulk-thumb img{width:100%;height:auto;display:block;vertical-align:middle}.everyalt-bulk-item .everyalt-bulk-checkbox{margin:6px 6px 0 0}.everyalt-bulk-item .everyalt-bulk-edit-link{display:inline-block;margin-top:4px;font-size:12px}.everyalt-bulk-item-status{display:block;margin-top:4px;font-size:11px;line-height:1.3}.everyalt-bulk-item-status.success{color:#00a32a}.everyalt-bulk-item-status.error{color:#b32d2e}.everyalt-review-wrap .description{margin-bottom:1em}.everyalt-review-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px;list-style:none;margin:1em 0;padding:0}.everyalt-review-item{border:1px solid #c3c4c7;border-radius:4px;padding:10px;background:#fff}.everyalt-review-item .everyalt-review-thumb{display:block;line-height:0;margin-bottom:8px}.everyalt-review-item .everyalt-review-thumb img{width:100%;height:auto;display:block}.everyalt-review-item .everyalt-review-edit-link{display:inline-block;font-size:12px;margin-bottom:6px}.everyalt-review-item .everyalt-review-alt-field{width:100%;margin:0 0 8px;padding:6px;font-size:13px;min-height:60px;display:block;box-sizing:border-box}.everyalt-review-item .everyalt-review-actions{display:flex;gap:6px;flex-wrap:wrap}.everyalt-review-item .everyalt-review-actions .button{margin:0}.everyalt-review-item .everyalt-review-status{display:block;margin-top:6px;font-size:12px}.everyalt-review-item .everyalt-review-status.success{color:#00a32a}.everyalt-review-item .everyalt-review-status.error{color:#b32d2e} \ No newline at end of file +*{box-sizing:inherit}.everyalt-settings-page{box-sizing:border-box;font-family:Helvetica Neue,Helvetica,Arial,Lucida Grande,sans-serif;font-size:14px;position:relative}#everyalt-history-images-empty{margin-left:auto;margin-right:auto;margin-top:2rem;max-width:1280px;padding:0 15px}#everyalt-history-images{grid-column-gap:10px;grid-row-gap:10px;display:grid;grid-template-columns:repeat(2,1fr);margin-top:2rem;padding-left:15px;padding-right:15px}@media (min-width:768px){#everyalt-history-images{grid-template-columns:repeat(3,1fr)}}@media (min-width:1200px){#everyalt-history-images{grid-template-columns:repeat(4,1fr)}}@media (min-width:1400px){#everyalt-history-images{grid-template-columns:repeat(5,1fr)}}@media (min-width:1800px){#everyalt-history-images{grid-template-columns:repeat(7,1fr)}}#everyalt-history-images img{height:auto;width:100%}#everyalt-history-images a{display:block;margin-bottom:1rem}#everyalt-history .components-snackbar-list__notice-container{display:flex;justify-content:end;padding-right:15px}#everyalt-custom-media-button .components-snackbar-list{bottom:15px;position:fixed;right:15px}#everyalt-custom-media-button .components-snackbar-list__notice-container{display:flex;justify-content:end}#everyalt-bulk-generator{margin-left:auto;margin-right:auto;max-width:1280px;padding:0 15px}#everyalt-bulk-generator .components-card{background-color:#fefefa;margin-top:2rem}#everyalt-bulk-generator .components-card .everyalt-progress-bar{margin-top:1rem}#everyalt-bulk-generator .components-card .everyalt-progress-bar .every-bar-wrapper{background-color:#fff;border:1px solid #b7c9d9;border-radius:3px;height:20px;overflow:hidden;position:relative}#everyalt-bulk-generator .components-card .everyalt-progress-bar .every-bar-bar{background-color:#0750b6;height:100%;left:0;position:absolute;top:0}#everyalt-bulk-generator .components-card .components-button.is-primary{background-color:#0750b6;text-transform:uppercase}#everyalt-bulk-generator table{margin-top:2rem;width:100%}#everyalt-bulk-generator table th{background-color:#00205b;color:#fff;font-size:.75rem;padding:5px;text-align:left;text-transform:uppercase;width:50%}#everyalt-bulk-generator table td{font-size:.75rem;padding:5px}#everyalt-bulk-generator table td.has_error{background-color:crimson;color:#fff}#everyalt-bulk-generator table tr.even{background-color:#e8e8e8}#everyalt-bulk-generator .everyalt-spinner{display:flex;justify-content:center;text-align:center}#everyalt-bulk-generator .everyalt-spinner-container{text-align:center}#everyalt-bulk-generator .completed-message{padding:20px;text-align:center}#everyalt-bulk-generator .completed-message h2{color:#00205b}#everyalt-plugin .components-snackbar-list{bottom:15px;left:0;position:fixed;width:100%}#everyalt-plugin .is-primary{background-color:#0750b6}#everyalt-plugin .every_alt_http_password_holder{margin-top:50px}#everyalt-plugin .every_alt_http_password_holder .components-base-control__help{margin-top:-15px}#everyalt-plugin .every_alt_http_password_holder h2{margin-bottom:5px}#everyalt-plugin .everyalt-main{padding:0 15px}#everyalt-plugin .components-form-toggle.is-checked .components-form-toggle__track{background-color:#0750b6}#everyalt-plugin .components-card{background-color:#fefefa;margin-top:2rem}#everyalt-plugin .components-card .everyalt-progress-bar{margin-top:2rem}#everyalt-plugin .components-card .everyalt-progress-bar .every-bar-wrapper{background-color:#fff;border:1px solid #b7c9d9;border-radius:3px;height:20px;overflow:hidden;position:relative}#everyalt-plugin .components-card .everyalt-progress-bar .every-bar-bar{background-color:#0750b6;height:100%;left:0;position:absolute;top:0}#everyalt-plugin .components-card .components-toggle-control{margin-top:2rem}#everyalt-plugin .components-card .components-base-control .components-base-control__field{align-items:center;display:flex;margin-bottom:10px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field{width:100%}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field{display:block}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field label{font-weight:600;margin-bottom:10px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field input{border:1px solid #b7c9d9;border-radius:3px;box-shadow:none;color:#444;font-size:14px;height:40px;margin:0 0 10px;padding:9px 15px;width:100%}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field input:disabled{background:#f6f6f6;border-color:#eee;color:#bbb}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field input:focus{border-color:#00a0d2;box-shadow:0 0 0 1px #00a0d2;outline:2px solid transparent;outline-offset:-2px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group{align-items:center;display:flex;margin:2px 0 15px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group .is-primary{background-color:#0750b6;margin-right:10px}#everyalt-plugin .components-snackbar-list__notice-container{display:flex;justify-content:end;padding-right:15px}#everyalt-plugin .components-notice{margin:0;width:100%}#everyalt-plugin .components-placeholder{background:#f6f6f6;box-shadow:none}#everyalt-plugin .components-placeholder .components-placeholder__fieldset{display:flex;justify-content:center}#everyalt-plugin .everyalt-main-error{margin-top:2rem}#everyalt-plugin .everyalt-main{margin-left:auto;margin-right:auto;max-width:1280px}#everyalt-plugin .everyalt-main .components-panel__body{background:#fff;border:1px solid #e2e4e7;margin:25px 0}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__body-toggle{border-bottom:1px solid #d6e2ed}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row{margin:0;padding:10px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control .components-base-control__field{align-items:center;display:flex;margin-bottom:10px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field{width:100%}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field{display:block}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field label{font-weight:600;margin-bottom:10px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field input{border:1px solid #b7c9d9;border-radius:3px;box-shadow:none;color:#444;font-size:14px;height:40px;margin:0 0 10px;padding:9px 15px;width:100%}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field input:disabled{background:#f6f6f6;border-color:#eee;color:#bbb}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field input:focus{border-color:#00a0d2;box-shadow:0 0 0 1px #00a0d2;outline:2px solid transparent;outline-offset:-2px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group{align-items:center;display:flex;margin:2px 0 15px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group .is-primary{margin-right:10px}#everyalt-plugin .everyalt-main .everyalt-info{padding:0 10px 10px}#everyalt-plugin .everyalt-main .everyalt-info h2{font-weight:300;margin-bottom:10px}#everyalt-plugin .everyalt-main .everyalt-info p{margin:0 0 1.5em}#everyalt-plugin .everyalt-main .everyalt-info .everyalt-info-button-group{display:flex}#everyalt-plugin .everyalt-main .everyalt-info .everyalt-info-button-group .is-default{font-size:14px;height:auto;padding:2px 20px}#everyalt-plugin .everyalt-main .everyalt-info .everyalt-info-button-group .is-default:first-child{margin-right:15px}#every_alt_pagination{display:flex;font-size:.85rem;font-weight:700;justify-content:center;margin-top:2rem}#every_alt_pagination a.page-numbers{background-color:#0750b6;color:#fff;display:inline-block;margin-right:-4px;min-width:20px;padding:8px;text-align:center;text-decoration:none}#every_alt_pagination a.page-numbers:hover{background-color:#00205b}#every_alt_pagination a.next,#every_alt_pagination a.prev{background-color:transparent;color:#0750b6;padding:0}#every_alt_pagination a.next:hover,#every_alt_pagination a.prev:hover{background-color:transparent;color:#00205b}#every_alt_pagination a.next{margin-left:10px}#every_alt_pagination a.prev{margin-right:10px}#every_alt_pagination span.current{background-color:#00205b;color:#fff;display:inline-block;margin-right:-4px;min-width:20px;padding:8px;text-align:center}#everyalt-main-error{background-color:#fefefa;margin-left:auto;margin-right:auto;margin-top:2rem;max-width:1280px;padding:15px}#everyalt-main-error h2{color:#00205b;text-align:center!important}#everyalt-gutenber-button p{font-size:.75rem}.everyalt-progress.hidden,.hidden{display:none!important}.everyalt-settings-wrap .form-table,.everyalt-bulk-wrap table,.everyalt-history-wrap table{margin-top:1em}.everyalt-log-detail{max-width:480px;vertical-align:top}.everyalt-log-detail-inner{max-height:280px;overflow-y:auto;overflow-x:auto;padding:6px 8px;background:#f6f7f7;border:1px solid #c3c4c7;border-radius:3px;font-size:12px;line-height:1.4;white-space:pre-wrap;word-break:break-word}.everyalt-key-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.everyalt-key-row .regular-text{flex:1;min-width:200px}.everyalt-bulk-actions{margin:1em 0}.everyalt-bulk-actions .button{margin-right:8px}.everyalt-bulk-progress{margin:1em 0;padding:1em;background:#f6f7f7;border:1px solid #c3c4c7;border-radius:3px}.everyalt-bulk-progress-bar{height:22px;background:#fff;border:1px solid #c3c4c7;border-radius:3px;overflow:hidden}.everyalt-bulk-progress-bar span{display:block;height:100%;background:#2271b1;transition:width .2s}.everyalt-bulk-progress-log{margin:.5em 0 0;padding-left:1.5em;max-height:200px;overflow-y:auto;font-size:13px}.everyalt-bulk-progress-log li.success{color:#00a32a}.everyalt-bulk-progress-log li.error{color:#b32d2e}.everyalt-bulk-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px;list-style:none;margin:1em 0;padding:0}.everyalt-bulk-item label{display:block;cursor:pointer}.everyalt-bulk-item .everyalt-bulk-thumb{display:block;line-height:0}.everyalt-bulk-item .everyalt-bulk-thumb img{width:100%;height:auto;display:block;vertical-align:middle}.everyalt-bulk-item .everyalt-bulk-checkbox{margin:6px 6px 0 0}.everyalt-bulk-item .everyalt-bulk-edit-link{display:inline-block;margin-top:4px;font-size:12px}.everyalt-bulk-item-status{display:block;margin-top:4px;font-size:11px;line-height:1.3}.everyalt-bulk-item-status.success{color:#00a32a}.everyalt-bulk-item-status.error{color:#b32d2e}.everyalt-review-wrap .description{margin-bottom:1em}.everyalt-review-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px;list-style:none;margin:1em 0;padding:0}.everyalt-review-item{border:1px solid #c3c4c7;border-radius:4px;padding:10px;background:#fff}.everyalt-review-item .everyalt-review-thumb{display:block;line-height:0;margin-bottom:8px}.everyalt-review-item .everyalt-review-thumb img{width:100%;height:auto;display:block}.everyalt-review-item .everyalt-review-edit-link{display:inline-block;font-size:12px;margin-bottom:6px}.everyalt-review-item .everyalt-review-alt-field{width:100%;margin:0 0 8px;padding:6px;font-size:13px;min-height:60px;display:block;box-sizing:border-box}.everyalt-review-item .everyalt-review-actions{display:flex;gap:6px;flex-wrap:wrap}.everyalt-review-item .everyalt-review-actions .button{margin:0}.everyalt-review-item .everyalt-review-status{display:block;margin-top:6px;font-size:12px}.everyalt-review-item .everyalt-review-status.success{color:#00a32a}.everyalt-review-item .everyalt-review-status.error{color:#b32d2e}.everyalt-page-count{margin:1em 0 0;color:#646970}.everyalt-pagination{display:flex;flex-wrap:wrap;gap:4px;margin:1.5em 0}.everyalt-pagination .page-numbers{display:inline-block;min-width:30px;padding:4px 8px;text-align:center;border:1px solid #c3c4c7;border-radius:3px;background:#fff;text-decoration:none}.everyalt-pagination .page-numbers.current{background:#2271b1;border-color:#2271b1;color:#fff}.everyalt-pagination .page-numbers.dots{border-color:transparent;background:transparent}.everyalt-model-list{display:grid;gap:8px;max-width:640px}.everyalt-model-option{display:grid;grid-template-columns:auto 1fr;column-gap:8px;align-items:baseline;padding:10px 12px;border:1px solid #c3c4c7;border-radius:4px;background:#fff;cursor:pointer}.everyalt-model-option input{grid-row:span 3;margin:2px 0 0}.everyalt-model-option:has(input:checked){border-color:#2271b1;box-shadow:0 0 0 1px #2271b1}.everyalt-model-provider,.everyalt-model-price{color:#646970;font-size:13px}.everyalt-provider-key{max-width:640px;margin-bottom:1.5em}.everyalt-provider-key-title{display:flex;align-items:center;gap:10px;margin:0 0 8px;font-size:14px}.everyalt-key-status{font-size:12px;font-weight:400;padding:1px 8px;border-radius:10px}.everyalt-key-status.is-saved{background:#edfaef;color:#008a20}.everyalt-key-status.is-missing{background:#fcf9e8;color:#8a6d00}.everyalt-key-steps{margin:10px 0 8px 1.5em}.everyalt-key-steps li{margin-bottom:4px}.everyalt-privacy-note{margin-top:10px;padding:10px 12px;border-left:4px solid #00a32a;background:#f6f7f7}.everyalt-privacy-note p{margin:0 0 6px}.everyalt-privacy-note p:last-child{margin-bottom:0} \ No newline at end of file diff --git a/admin/js/everyalt-admin-simple.js b/admin/js/everyalt-admin-simple.js index 43d82b4..4ed59b5 100644 --- a/admin/js/everyalt-admin-simple.js +++ b/admin/js/everyalt-admin-simple.js @@ -8,6 +8,8 @@ var restNonce = typeof everyaltAdmin !== 'undefined' ? everyaltAdmin.restNonce : ''; var ajaxUrl = typeof everyaltAdmin !== 'undefined' ? everyaltAdmin.ajaxUrl : ''; var validateKeyNonce = typeof everyaltAdmin !== 'undefined' ? everyaltAdmin.validateKeyNonce : ''; + var i18n = (typeof everyaltAdmin !== 'undefined' && everyaltAdmin.i18n) || {}; + function t(key, fallback) { return i18n[key] || fallback; } function request(method, path, body) { var url = restUrl.replace(/\/$/, '') + path; @@ -27,12 +29,35 @@ }); } - // Validate API key (Settings tab) - var validateKeyBtn = document.getElementById('everyalt-validate-key'); - if (validateKeyBtn) { - var resultEl = document.getElementById('everyalt-validate-result'); + // Settings: show only the API key block for the selected model's provider. + var modelRadios = document.querySelectorAll('.everyalt-model-list input[type="radio"]'); + function showProviderKey() { + var checked = document.querySelector('.everyalt-model-list input[type="radio"]:checked'); + var provider = checked ? checked.getAttribute('data-provider') : ''; + document.querySelectorAll('.everyalt-provider-key').forEach(function(block) { + block.classList.toggle('hidden', block.getAttribute('data-provider') !== provider); + }); + } + if (modelRadios.length) { + modelRadios.forEach(function(radio) { radio.addEventListener('change', showProviderKey); }); + showProviderKey(); + } + + // Validate API key (Settings tab), one button per provider. + document.querySelectorAll('.everyalt-validate-key').forEach(function(validateKeyBtn) { + var block = validateKeyBtn.closest('.everyalt-provider-key'); + var resultEl = block ? block.querySelector('.everyalt-validate-result') : null; + var provider = validateKeyBtn.getAttribute('data-provider'); + function showResult(ok, message) { + resultEl.style.display = 'block'; + resultEl.className = 'everyalt-validate-result notice ' + (ok ? 'notice-success' : 'notice-error'); + resultEl.textContent = ''; + var p = document.createElement('p'); + p.textContent = message; + resultEl.appendChild(p); + } validateKeyBtn.addEventListener('click', function() { - var keyInput = document.getElementById('every_alt_openai_key'); + var keyInput = block ? block.querySelector('input[type="password"]') : null; var key = keyInput ? keyInput.value.trim() : ''; if (!resultEl) return; resultEl.style.display = 'none'; @@ -41,6 +66,7 @@ var formData = new FormData(); formData.append('action', 'everyalt_validate_key'); formData.append('nonce', validateKeyNonce); + formData.append('provider', provider); formData.append('key', key); fetch(ajaxUrl, { method: 'POST', @@ -50,22 +76,15 @@ .then(function(r) { return r.json(); }) .then(function(data) { validateKeyBtn.disabled = false; - resultEl.style.display = 'block'; - var msg = (data.data && data.data.message) ? data.data.message : (data.success ? '' : 'Error'); - resultEl.className = 'everyalt-validate-result notice ' + (data.success ? 'notice-success' : 'notice-error'); - resultEl.textContent = ''; - var p = document.createElement('p'); - p.textContent = msg; - resultEl.appendChild(p); + var msg = (data.data && data.data.message) ? data.data.message : (data.success ? '' : t('error', 'Error')); + showResult(!!data.success, msg); }) .catch(function() { validateKeyBtn.disabled = false; - resultEl.style.display = 'block'; - resultEl.className = 'everyalt-validate-result notice notice-error'; - resultEl.innerHTML = '

Request failed.

'; + showResult(false, t('requestFailed', 'Request failed')); }); }); - } + }); // Bulk: Select all / Select none var selectAllBtn = document.getElementById('everyalt-bulk-select-all'); @@ -127,13 +146,13 @@ var success = data && data.success; var msg = success ? (data.alt_text || '') : (data && data.message) ? data.message : ''; if (statusEl) { - statusEl.textContent = success ? '\u2713 ' + (data.alt_text || '') : '\u2717 ' + (data.message || 'Error'); + statusEl.textContent = success ? '\u2713 ' + (data.alt_text || '') : '\u2717 ' + (data.message || t('error', 'Error')); statusEl.className = 'everyalt-bulk-item-status ' + (success ? 'success' : 'error'); } if (progressLog) { var li = document.createElement('li'); li.className = success ? 'success' : 'error'; - li.textContent = '#' + id + ': ' + (success ? (data.alt_text || '') : (data.message || 'Error')); + li.textContent = '#' + id + ': ' + (success ? (data.alt_text || '') : (data.message || t('error', 'Error'))); progressLog.appendChild(li); } if (success && itemEl) { @@ -146,7 +165,7 @@ .catch(function(err) { done++; updateProgress(); - var errMsg = err && err.message ? err.message : 'Request failed'; + var errMsg = err && err.message ? err.message : t('requestFailed', 'Request failed'); if (statusEl) { statusEl.textContent = '\u2717 ' + errMsg; statusEl.className = 'everyalt-bulk-item-status error'; @@ -177,13 +196,12 @@ btn.disabled = true; request('POST', '/everyalt-api/v1/save_alt', { media_id: mediaId, - log_id: 0, alt_text: altText }) .then(function() { btn.disabled = false; if (statusEl) { - statusEl.textContent = 'Saved!'; + statusEl.textContent = t('saved', 'Saved!'); statusEl.className = 'everyalt-review-status success'; setTimeout(function() { statusEl.textContent = ''; statusEl.className = 'everyalt-review-status'; }, 2000); } @@ -191,7 +209,7 @@ .catch(function(err) { btn.disabled = false; if (statusEl) { - statusEl.textContent = 'Error: ' + (err && err.message ? err.message : 'Save failed'); + statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (err && err.message ? err.message : t('saveFailed', 'Save failed')); statusEl.className = 'everyalt-review-status error'; } }); @@ -213,13 +231,13 @@ if (data && data.success && data.alt_text !== undefined) { if (textarea) textarea.value = data.alt_text; if (statusEl) { - statusEl.textContent = 'Regenerated!'; + statusEl.textContent = t('regenerated', 'Regenerated!'); statusEl.className = 'everyalt-review-status success'; setTimeout(function() { statusEl.textContent = ''; statusEl.className = 'everyalt-review-status'; }, 2000); } } else { if (statusEl) { - statusEl.textContent = 'Error: ' + (data && data.message ? data.message : 'Regenerate failed'); + statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (data && data.message ? data.message : t('regenerateFailed', 'Regenerate failed')); statusEl.className = 'everyalt-review-status error'; } } @@ -227,7 +245,7 @@ .catch(function(err) { btn.disabled = false; if (statusEl) { - statusEl.textContent = 'Error: ' + (err && err.message ? err.message : 'Request failed'); + statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (err && err.message ? err.message : t('requestFailed', 'Request failed')); statusEl.className = 'everyalt-review-status error'; } }); @@ -293,13 +311,13 @@ updateProgress(); var success = data && data.success; if (statusEl) { - statusEl.textContent = success ? '✓ ' + (data.title || '') : '✗ ' + (data.message || 'Error'); + statusEl.textContent = success ? '✓ ' + (data.title || '') : '✗ ' + (data.message || t('error', 'Error')); statusEl.className = 'everyalt-bulk-item-status ' + (success ? 'success' : 'error'); } if (titleProgressLog) { var li = document.createElement('li'); li.className = success ? 'success' : 'error'; - li.textContent = '#' + id + ': ' + (success ? (data.title || '') : (data.message || 'Error')); + li.textContent = '#' + id + ': ' + (success ? (data.title || '') : (data.message || t('error', 'Error'))); titleProgressLog.appendChild(li); } if (success && itemEl) { @@ -312,7 +330,7 @@ .catch(function(err) { done++; updateProgress(); - var errMsg = err && err.message ? err.message : 'Request failed'; + var errMsg = err && err.message ? err.message : t('requestFailed', 'Request failed'); if (statusEl) { statusEl.textContent = '✗ ' + errMsg; statusEl.className = 'everyalt-bulk-item-status error'; @@ -348,7 +366,7 @@ .then(function() { btn.disabled = false; if (statusEl) { - statusEl.textContent = 'Saved!'; + statusEl.textContent = t('saved', 'Saved!'); statusEl.className = 'everyalt-review-status success'; setTimeout(function() { statusEl.textContent = ''; statusEl.className = 'everyalt-review-status'; }, 2000); } @@ -356,7 +374,7 @@ .catch(function(err) { btn.disabled = false; if (statusEl) { - statusEl.textContent = 'Error: ' + (err && err.message ? err.message : 'Save failed'); + statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (err && err.message ? err.message : t('saveFailed', 'Save failed')); statusEl.className = 'everyalt-review-status error'; } }); @@ -378,13 +396,13 @@ if (data && data.success && data.title !== undefined) { if (textarea) textarea.value = data.title; if (statusEl) { - statusEl.textContent = 'Regenerated!'; + statusEl.textContent = t('regenerated', 'Regenerated!'); statusEl.className = 'everyalt-review-status success'; setTimeout(function() { statusEl.textContent = ''; statusEl.className = 'everyalt-review-status'; }, 2000); } } else { if (statusEl) { - statusEl.textContent = 'Error: ' + (data && data.message ? data.message : 'Regenerate failed'); + statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (data && data.message ? data.message : t('regenerateFailed', 'Regenerate failed')); statusEl.className = 'everyalt-review-status error'; } } @@ -392,7 +410,7 @@ .catch(function(err) { btn.disabled = false; if (statusEl) { - statusEl.textContent = 'Error: ' + (err && err.message ? err.message : 'Request failed'); + statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (err && err.message ? err.message : t('requestFailed', 'Request failed')); statusEl.className = 'everyalt-review-status error'; } }); diff --git a/admin/js/everyalt-gutenberg-button.js b/admin/js/everyalt-gutenberg-button.js index fbf800f..fde3ab3 100644 --- a/admin/js/everyalt-gutenberg-button.js +++ b/admin/js/everyalt-gutenberg-button.js @@ -14,6 +14,18 @@ var PanelBody = wp.components.PanelBody; var Button = wp.components.Button; + var i18n = (typeof everyaltBlock !== 'undefined' && everyaltBlock.i18n) || {}; + function t(key, fallback) { return i18n[key] || fallback; } + + // Show a dismissible snackbar/notice in the editor; failures used to be swallowed silently. + function notify(type, message) { + if (!wp.data || !wp.data.dispatch) return; + var notices = wp.data.dispatch('core/notices'); + if (!notices) return; + var create = type === 'error' ? notices.createErrorNotice : notices.createSuccessNotice; + create(message, { id: 'everyalt-generate', type: 'snackbar', isDismissible: true }); + } + function EveryAltImageEdit(BlockEdit) { return function (props) { if (props.name !== 'core/image') { @@ -33,15 +45,18 @@ wp.apiFetch({ path: 'everyalt-api/v1/bulk_generate_alt', method: 'POST', - data: { media_id: attachmentId }, - headers: { 'Content-Type': 'application/json' } + data: { media_id: attachmentId } }).then(function (res) { setIsBusy(false); if (res && res.success && res.alt_text) { setAttributes({ alt: res.alt_text }); + notify('success', t('generated', 'Alt text generated.')); + } else { + notify('error', (res && res.message) ? res.message : t('failed', 'Could not generate alt text.')); } - }).catch(function () { + }).catch(function (err) { setIsBusy(false); + notify('error', t('errorPrefix', 'Error:') + ' ' + ((err && err.message) ? err.message : t('failed', 'Could not generate alt text.'))); }); } @@ -49,7 +64,7 @@ el(BlockEdit, props), el(InspectorControls, { key: 'everyalt' }, el(PanelBody, { - title: 'EveryAlt', + title: t('panelTitle', 'EveryAlt'), initialOpen: true, className: 'everyalt-inspector-panel' }, @@ -62,9 +77,9 @@ isBusy: isBusy, disabled: isBusy, style: { marginTop: '8px' } - }, 'Generate alt text with EveryAlt') + }, t('button', 'Generate alt text with EveryAlt')) : el('p', { className: 'everyalt-gutenberg-help', style: { margin: 0, fontSize: '12px', color: '#757575' } }, - 'Select or upload an image to generate alt text.') + t('selectImage', 'Select or upload an image to generate alt text.')) ) ) ); diff --git a/admin/js/everyalt-media-simple.js b/admin/js/everyalt-media-simple.js index d5ece2e..761ce1a 100644 --- a/admin/js/everyalt-media-simple.js +++ b/admin/js/everyalt-media-simple.js @@ -8,6 +8,8 @@ var restUrl = (config.restUrl || '').replace(/\/$/, ''); var restNonce = config.restNonce || ''; var mediaId = config.mediaId || 0; + var i18n = config.i18n || {}; + function t(key, fallback) { return i18n[key] || fallback; } if (!restUrl || !mediaId) return; @@ -17,7 +19,7 @@ var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'button'; - btn.textContent = 'Generate alt text with EveryAlt'; + btn.textContent = t('button', 'Generate alt text with EveryAlt'); wrap.appendChild(btn); var msg = document.createElement('p'); @@ -27,7 +29,7 @@ btn.addEventListener('click', function() { btn.disabled = true; - msg.textContent = 'Generating…'; + msg.textContent = t('generating', 'Generating…'); msg.className = 'everyalt-media-message'; fetch(restUrl + '/everyalt-api/v1/bulk_generate_alt', { @@ -46,20 +48,20 @@ btn.disabled = false; var success = data && data.success && data.alt_text; if (success) { - msg.textContent = 'Alt text generated.'; + msg.textContent = t('generated', 'Alt text generated.'); msg.className = 'everyalt-media-message notice notice-success'; var altField = document.getElementById('attachment_alt') || document.querySelector('textarea[name*="_wp_attachment_image_alt"]') || document.querySelector('input[name*="_wp_attachment_image_alt"]'); if (altField) { altField.value = data.alt_text; } } else { - msg.textContent = (data && data.message) ? data.message : 'Could not generate alt text.'; + msg.textContent = (data && data.message) ? data.message : t('failed', 'Could not generate alt text.'); msg.className = 'everyalt-media-message notice notice-error'; } }) .catch(function(err) { btn.disabled = false; - msg.textContent = 'Error: ' + (err.message || 'Request failed'); + msg.textContent = t('errorPrefix', 'Error:') + ' ' + (err.message || t('failed', 'Could not generate alt text.')); msg.className = 'everyalt-media-message notice notice-error'; }); }); diff --git a/admin/partials/everyalt-options-display.php b/admin/partials/everyalt-options-display.php index 9959a2f..c522b3a 100644 --- a/admin/partials/everyalt-options-display.php +++ b/admin/partials/everyalt-options-display.php @@ -41,7 +41,10 @@

-

+ +

@@ -50,21 +53,127 @@
+ ' . esc_html( $text ) . ''; + }; + ?> + + + + - + @@ -73,25 +182,7 @@ @@ -127,17 +218,12 @@ - error ) ? $this->error : false; - ?>

Review Alt Text tab.', 'everyalt' ), esc_url( add_query_arg( 'tab', 'review', $base_url ) ) ) ); ?>

- -

- -

- + +

+

@@ -150,8 +236,9 @@

+

+ +
+ @@ -177,14 +267,15 @@

- -

+ +

- +

+

+ +
+
- error ) ? $this->error : false; - ?>

Review Image Titles tab.', 'everyalt' ), esc_url( add_query_arg( 'tab', 'review_title', $base_url ) ) ) ); ?>

- -

- -

- + +

+

@@ -231,8 +320,9 @@

+

+ +
+ @@ -258,14 +351,15 @@

- -

+ +

- +

+

+ +
+
@@ -304,6 +401,7 @@ + @@ -346,6 +444,7 @@ } ?> +