Skip to content

Conversation

@arifulhoque7
Copy link
Contributor

@arifulhoque7 arifulhoque7 commented Dec 1, 2025

Summary

This PR enhances the AI settings page with several user-requested features to improve the configuration experience and ensure API keys are valid before use.

Changes Made

1. Temperature Field (Dynamic Control)

  • Added temperature input field with HTML5 number validation (range: 0.0-1.0, step: 0.1)
  • Temperature value is saved to wpuf_ai[temperature] and used in all AI API calls
  • FormGenerator loads temperature from settings as default when generating forms
  • Real-time client-side validation with visual feedback (red border for invalid, green for valid)

Files Modified:

  • includes/functions/settings-options.php (lines 666-673, 1246-1267)
  • includes/AI/FormGenerator.php (lines 113-116)

2. Masked API Key Display

  • API keys now show only first 4 and last 4 characters (e.g., AIza**********************xyz)
  • Added "Change" button to allow editing existing keys
  • Masked display switches to password input when editing
  • Full security maintained - actual keys remain in password fields

Files Modified:

  • includes/functions/settings-options.php (lines 760-788, 867-900)

3. Test Connection Button

  • Added "Test Connection" button with spinner animation
  • Tests the currently selected provider without requiring save
  • Makes minimal API call (5 tokens) to verify API key validity
  • Shows provider-specific success/error messages
  • Auto-enables model field and refreshes models on success
  • Success messages auto-hide after 5 seconds, errors stay visible

Implementation:

  • Provider-specific test methods for OpenAI, Anthropic, and Google
  • Accepts API key, provider, and model from UI (not just saved settings)
  • Proper timeout clearing to ensure messages always display
  • Dynamic provider selection - tests whichever provider is currently selected

Files Modified:

  • includes/AI/FormGenerator.php (lines 863-1074) - Added test_connection() and provider-specific test methods
  • includes/AI/RestController.php (lines 125-146, 405-423) - Added REST endpoint with parameters
  • includes/functions/settings-options.php (lines 787-806, 984-1066) - Frontend AJAX integration

4. Reorganized Field Order

New logical workflow order:

  1. AI Provider selection
  2. API Key entry (with test button and masked display)
  3. AI Model selection (enabled after valid API key)
  4. Temperature setting

Files Modified:

  • includes/functions/settings-options.php (lines 640-674)

Technical Details

Temperature Handling:

  • Stored in wpuf_ai['temperature'] option
  • Default: 0.7 (balanced creativity)
  • Validated and clamped: max(0.0, min(1.0, $value))
  • Passed to API calls via $options['temperature']

Test Connection Flow:

  1. JavaScript reads selected provider, API key, and model from UI
  2. Sends POST to /wpuf/v1/ai-form-builder/test with JSON payload
  3. Backend temporarily overrides provider/model/key for testing
  4. Makes minimal API request (5 tokens)
  5. Returns success/failure with specific error messages
  6. Frontend shows result with auto-hide for success

Security:

  • All inputs sanitized with sanitize_text_field()
  • API keys never exposed in responses
  • Nonce verification on all AJAX requests
  • Capability checks: check_permission() for test endpoint

Test Plan

  • Temperature field displays current value
  • Temperature validation prevents values outside 0.0-1.0
  • Temperature saves and applies to AI API calls
  • API key shows masked format when exists
  • "Change" button reveals password input
  • Test connection works for OpenAI
  • Test connection works for Anthropic
  • Test connection works for Google
  • Test uses currently selected provider (not saved)
  • Success/error messages display on every test
  • Model dropdown enables after successful test
  • Models refresh after successful connection
  • All existing functionality preserved

Breaking Changes

None. All changes are backward compatible.


Files Changed

  • includes/functions/settings-options.php (210 lines modified)
  • includes/AI/FormGenerator.php (27 lines modified)
  • includes/AI/RestController.php (31 lines modified)

Closes 1740

Summary by CodeRabbit

  • New Features
    • Test Connection now accepts optional provider, model, and API key overrides and returns unified status messages.
    • API Key management UI: masked display, Change button, Test Connection flow with spinner and status feedback.
    • Temperature setting for AI responses (numeric 0–1, default 0.7) with validation and description.
    • Live model list refresh and UI gating based on selected provider and valid API key.

✏️ Tip: You can customize this high-level summary in your review settings.

@arifulhoque7 arifulhoque7 requested a review from sapayth December 1, 2025 06:08
@arifulhoque7 arifulhoque7 self-assigned this Dec 1, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 1, 2025

Walkthrough

Adds optional api_key/provider/model parameters to AI connection tests, introduces provider-specific test helpers and orchestration, changes the REST test route to POST with args, and replaces the settings API key input with a masked interactive widget plus a temperature field.

Changes

Cohort / File(s) Summary
Provider API testing
includes/AI/FormGenerator.php
test_connection( $api_key = '', $provider = '', $model = '' ) added. New private orchestration test_provider_api() plus provider handlers test_openai_connection(), test_anthropic_connection(), test_google_connection(). Restores original provider/model/api_key, applies per-request temperature defaulting, and unifies error propagation.
REST API configuration
includes/AI/RestController.php
Test route changed from READABLE to CREATABLE (POST). Adds api_key, provider, model args with types/sanitization; handler forwards these params to FormGenerator::test_connection().
Settings UI and rendering
includes/functions/settings-options.php
Replaces static API key input with wpuf_ai_api_key_field() (masked display, Change/Test Connection UI, status area, JS for masking and REST test/model refresh). Adds wpuf_ai_temperature_field() and temperature setting (default 0.7) with client-side validation and UI behaviors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

needs: dev review

Suggested reviewers

  • sapayth

Poem

🐇 I hopped through keys both masked and bright,

I nudged the clouds with gentle byte,
OpenAI, Claude, and Gemini said hi,
A tiny ping — a model's reply,
I nudge the temp and watch ideas take flight.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main changes: adding temperature control, API key masking, and connection testing to AI settings.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@arifulhoque7 arifulhoque7 added needs: dev review This PR needs review by a developer needs: testing labels Dec 1, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
includes/AI/FormGenerator.php (1)

873-922: Test connection with overrides properly manages state.

The implementation correctly:

  1. Reloads settings to get latest data
  2. Stores original values before overriding
  3. Validates API key presence
  4. Restores original values after testing
  5. Handles exceptions with proper error response

However, the catch block doesn't restore original values on exception.

The exception handler should restore original values to maintain consistency:

         } catch (\Exception $e) {
+            // Restore original values on exception
+            $this->current_provider = $original_provider;
+            $this->current_model = $original_model;
+            $this->api_key = $original_key;
+
             return [
                 'success' => false,
                 'provider' => $this->current_provider,
                 'message' => $e->getMessage()
             ];
         }
🧹 Nitpick comments (1)
includes/functions/settings-options.php (1)

811-814: Hidden inputs expose all provider API keys in page source.

All three provider API keys are stored in hidden input fields in the HTML source. While they're not directly visible, they can be viewed via browser dev tools or "View Source". This is acceptable for an admin-only settings page but worth noting.

Consider fetching non-current provider keys via AJAX only when the provider is switched, rather than embedding all keys in the DOM. This reduces the attack surface if an admin's browser session is compromised.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d91cfac and b31fe99.

📒 Files selected for processing (3)
  • includes/AI/FormGenerator.php (3 hunks)
  • includes/AI/RestController.php (2 hunks)
  • includes/functions/settings-options.php (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
includes/AI/RestController.php (1)
includes/AI/FormGenerator.php (1)
  • test_connection (873-923)
includes/AI/FormGenerator.php (2)
Lib/WeDevs_Settings_API.php (1)
  • get_option (616-625)
includes/AI/RestController.php (1)
  • test_connection (405-423)
includes/functions/settings-options.php (1)
Lib/WeDevs_Settings_API.php (1)
  • get_option (616-625)
🪛 PHPMD (2.15.0)
includes/functions/settings-options.php

1265-1265: Avoid unused parameters such as '$args'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (12)
includes/functions/settings-options.php (6)

650-656: LGTM - Field definition for API key with callback.

The field configuration correctly uses the callback type to render a custom API key UI component with masking and test connection functionality.


667-673: LGTM - Temperature field configuration.

The temperature field is properly defined with callback type and sensible default of 0.7.


757-769: API key masking logic is sound.

The masking implementation correctly shows first 4 and last 4 characters for keys longer than 8 characters, and fully masks shorter keys. This provides good UX while maintaining security.


1014-1026: AJAX test connection properly uses REST API with nonce.

The implementation correctly:

  • Uses POST method for the test endpoint
  • Sends JSON content type
  • Includes WP REST nonce via X-WP-Nonce header
  • Passes provider, API key, and model for testing

1265-1285: Temperature field implementation is correct; unused parameter is acceptable.

The static analysis flags $args as unused, but this is a required signature for WordPress Settings API callbacks. The implementation properly clamps values between 0.0 and 1.0 and uses HTML5 validation attributes.


790-798: Verify that proper capability checks are in place before this settings page is rendered.

While esc_attr() is properly used on the API key value, confirming admin-only access with capability checks (such as current_user_can() with appropriate capabilities) is essential to validate the claim that the XSS risk is minimal in this context.

includes/AI/RestController.php (2)

125-146: Good: POST method for test endpoint with optional parameters.

Changing from READABLE to CREATABLE (GET to POST) is the correct approach since:

  1. The request includes sensitive API keys that shouldn't be in URL/logs
  2. The operation has side effects (makes external API calls)
  3. Parameters are properly typed and sanitized

The sanitize_callback for api_key uses sanitize_text_field which is appropriate for API key strings.


405-423: LGTM - Test connection handler correctly passes parameters.

The handler properly extracts parameters from the request and forwards them to FormGenerator::test_connection(). The try-catch ensures errors are handled gracefully.

includes/AI/FormGenerator.php (4)

98-116: Good: Temperature defaults properly loaded from settings.

The implementation correctly:

  1. Loads settings with get_option('wpuf_ai', [])
  2. Only sets temperature if not already provided in options
  3. Uses floatval() for type safety with fallback to 0.7

932-950: LGTM - Provider dispatcher with clean switch statement.

The test_provider_api() method cleanly routes to provider-specific test methods and handles unknown providers gracefully.


955-995: OpenAI test connection is well-implemented.

The method uses a minimal request (max_tokens: 5, simple "Hi" message) to minimize API costs while still validating credentials. Error handling properly extracts the error message from the response body.


1000-1041: Anthropic test connection follows same pattern - good consistency.

The implementation mirrors OpenAI's approach with appropriate Anthropic-specific headers (x-api-key, anthropic-version).

@Rubaiyat-E-Mohammad
Copy link
Contributor

Token limit configuration is missing @arifulhoque7 vai
image

@arifulhoque7
Copy link
Contributor Author

Token limit configuration is missing @arifulhoque7 vai image

Token limit configuration can cause broken JSON in the form builder, thus we can't implement it here @Rubaiyat-E-Mohammad vai

@Rubaiyat-E-Mohammad Rubaiyat-E-Mohammad added QA Approved This PR is approved by the QA team and removed bug labels Dec 2, 2025
Copy link
Member

@sapayth sapayth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • use sprintf() to make translatable string with placeholder like urls, html attributes etc.
  • follow our projects coding standard for spacing, indentation etc.

[
'name' => 'api_key_current',
'label' => __( 'API Key', 'wp-user-frontend' ),
'desc' => __( 'Enter your AI service API key. Need help finding your <a href="https://platform.openai.com/api-keys" target="_blank" class="wpuf-api-key-link" data-openai="https://platform.openai.com/api-keys" data-anthropic="https://console.anthropic.com/settings/keys" data-google="https://aistudio.google.com/app/apikey" style="text-decoration: underline;">API Key?</a>', 'wp-user-frontend' ),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use sprintf() to make translatable string with placeholder like urls, html attributes etc.

@sapayth sapayth added needs: author reply This PR needs author feedback or code changes and removed needs: dev review This PR needs review by a developer labels Dec 3, 2025
Refactored AI provider connection test methods for better code consistency and error handling. Updated the API key settings field to use sprintf for localized help links, improving translation and maintainability.
@arifulhoque7
Copy link
Contributor Author

  • use sprintf() to make translatable string with placeholder like urls, html attributes etc.
  • follow our projects coding standard for spacing, indentation etc.

done @sapayth vai

@arifulhoque7 arifulhoque7 added needs: dev review This PR needs review by a developer and removed needs: author reply This PR needs author feedback or code changes labels Dec 4, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
includes/AI/FormGenerator.php (1)

567-599: Avoid sending Google API key in query string for main generation call

generate_with_google() still appends the API key as a ?key= query parameter on $endpoint, while test_google_connection() correctly uses the x-goog-api-key header. For server‑side code, using the header for both is preferable to avoid exposing keys in logs and intermediaries.

Refactor generate_with_google() to remove '?key=' . $this->api_key from the URL and instead include x-goog-api-key in the headers array, mirroring test_google_connection().

🧹 Nitpick comments (3)
includes/AI/FormGenerator.php (2)

873-923: Improve robustness of state restoration in test_connection

The overall flow (reload settings, allow overrides, temporarily swap API key, then delegate to test_provider_api()) looks good. However, if an exception is ever thrown after $this->current_provider / $this->current_model / $this->api_key are overridden, the catch block returns an error but never restores the originals, unlike generate_form().

Wrapping restoration in a finally-style pattern (or duplicating restoration in the catch) would make this more future‑proof and consistent with generate_form()’s restoration logic.


955-995: Connection test helpers are logically correct; adjust array formatting to satisfy PHPCS

  • The three test helpers correctly:
    • Use provider‑appropriate endpoints and auth headers.
    • Send a minimal 5‑token request.
    • Normalize responses into a { success, provider, message } structure.
  • Static hint at ~964: in test_openai_connection(), the multi‑item associative array that includes 'messages' should be expanded so each value starts on a new line to match the project’s coding standard (e.g., break [ [ 'role' => 'user', 'content' => 'Hi' ] ], across lines).

This is cosmetic but will clear the PHPCS warning.

Also applies to: 1000-1041, 1046-1091

includes/functions/settings-options.php (1)

976-991: Temperature field implementation is sound; keep $args and server‑side clamping

  • The temperature input is correctly configured as an HTML5 <input type="number"> with min="0", max="1", and step="0.1", and the JS adds nice immediate visual feedback.
  • wpuf_ai_temperature_field() clamps the value to [0.0, 1.0] before output, which is good defense in depth and aligns with the backend usage in FormGenerator::generate_form().

The $args parameter is intentionally unused but required by the Settings API callback signature. If PHPMD’s warning becomes noisy, a minimal fix is to rename it to $_args or add a short comment (// Required by Settings API callback signature) to make the intent clear.

Also applies to: 1274-1294

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b31fe99 and 0cfd6a5.

📒 Files selected for processing (2)
  • includes/AI/FormGenerator.php (2 hunks)
  • includes/functions/settings-options.php (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
includes/AI/FormGenerator.php (2)
Lib/WeDevs_Settings_API.php (1)
  • get_option (616-625)
includes/AI/RestController.php (1)
  • test_connection (405-423)
includes/functions/settings-options.php (1)
Lib/WeDevs_Settings_API.php (1)
  • get_option (616-625)
🪛 GitHub Check: Run PHPCS inspection
includes/AI/FormGenerator.php

[failure] 99-99:
Expected 1 spaces before closing parenthesis; 0 found


[failure] 99-99:
Expected 1 spaces after opening parenthesis; 0 found


[warning] 964-964:
When a multi-item array uses associative keys, each value should start on a new line.

🪛 PHPMD (2.15.0)
includes/functions/settings-options.php

1274-1274: Avoid unused parameters such as '$args'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (3)
includes/AI/FormGenerator.php (1)

932-950: Provider dispatch helper looks correct and future‑proof

test_provider_api() cleanly dispatches by $this->current_provider and returns a standardized array, with an explicit “Unknown provider” branch. This is a nice central hook for additional providers later.

includes/functions/settings-options.php (2)

650-682: AI settings field definitions look consistent with intended UX

The new api_key_current callback field and temperature callback field are wired correctly (names, defaults, callbacks, descriptions), and the order (provider → key → model → temperature) matches the PR objective.


766-819: Fix Test Connection behavior for already‑saved keys and align model gating

The masking UI and per‑provider hidden key storage are well thought out, but there's a functional gap:

  • On initial load, when an API key already exists:
    • The masked display shows correctly.
    • #wpuf_ai_api_key_field is empty and hidden.
    • updateModelFieldState() is initialized with initialApiKey = $('#wpuf_ai_api_key_field').val(); (empty), so the model select is disabled and its description changed to "Please enter a valid API key…".
    • The Test Connection button uses var apiKey = $('#wpuf_ai_api_key_field').val(); (Line 1001–1002), so clicking it with a saved key but without pressing "Change" always yields "Please enter a valid API key", even though a valid key exists in the hidden input.

To make the UX match the spec ("test" should work with the configured key without forcing a change), I recommend:

  • Deriving the key for testing and model gating from the actual stored value, e.g.:
// When testing
var provider = $('input[name="wpuf_ai[ai_provider]"]:checked').val();
var apiKey = $('#wpuf_ai_' + provider + '_key').val() || $('#wpuf_ai_api_key_field').val();
  • Initializing state from the current provider's hidden key:
var initialProvider = $('input[name="wpuf_ai[ai_provider]"]:checked').val() || 'openai';
updateVisibleInputName(initialProvider);
updateApiKeyDisplay(initialProvider);
var initialApiKey = $('#wpuf_ai_' + initialProvider + '_key').val();
updateModelFieldState(initialApiKey);
$('#wpuf-test-connection-btn').prop('disabled', !initialApiKey || initialApiKey.trim().length < 10);

This keeps the masking behavior but lets admins test existing keys and have the model field correctly enabled when a valid key is already stored.

Comment on lines +120 to +100
// Using direct API implementation for AI providers

// Use manual AI provider implementation as fallback
switch ($this->current_provider) {
case 'openai':
$result = $this->generate_with_openai($prompt, $options);
break;

case 'anthropic':
$result = $this->generate_with_anthropic($prompt, $options);
break;

case 'google':
$result = $this->generate_with_google($prompt, $options);
break;

default:
throw new \Exception('Unsupported AI provider: ' . $this->current_provider);
}

// Restore original provider, model, and API key
$this->current_provider = $original_provider;
$this->current_model = $original_model;
$this->api_key = $original_api_key;

return $result;

} catch (\Exception $e) {
// Ensure full restoration even on exception
$this->current_provider = $original_provider ?? $this->current_provider;
$this->current_model = $original_model ?? $this->current_model;
$this->api_key = $original_api_key ?? $this->api_key;

return [
'success' => false,
'error' => true,
'message' => $e->getMessage(),
'provider' => $this->current_provider
];
}
}

/**
* Get model-specific parameter configuration
*
* This centralizes all model-specific parameter mappings for easy maintenance.
* Different AI providers and models have different parameter requirements:
*
* - Token parameters: 'max_tokens' vs 'max_completion_tokens' vs 'maxOutputTokens'
* - Temperature: Some models only support default temperature (1.0)
* - Response format: Some models don't support JSON mode
*
* @since 4.2.1
*
* @param string $provider Provider name (openai, anthropic, google)
* @param string $model Model name (e.g., gpt-5, claude-4.1-opus, gemini-2.5-pro)
* @return array Model configuration with parameter restrictions and requirements
*/
private function get_model_config($provider, $model) {
// Get configuration from centralized Config class
$config = Config::get_model_config($model);

// Return config if found, otherwise use safe defaults
if ($config !== null) {
return $config;
}

// Fallback defaults for each provider if model not found
$defaults = [
'openai' => [
'token_param' => 'max_tokens',
'token_location' => 'body',
'supports_json_mode' => true,
'supports_custom_temperature' => true
],
'anthropic' => [
'token_param' => 'max_tokens',
'token_location' => 'body',
'supports_json_mode' => true,
'supports_custom_temperature' => true
],
'google' => [
'token_param' => 'maxOutputTokens',
'token_location' => 'generationConfig',
'supports_json_mode' => true,
'supports_custom_temperature' => true
]
];

return $defaults[$provider] ?? $defaults['openai'];
}


/**
* Generate form using OpenAI
*
* @since 4.2.1
*
* @param string $prompt User prompt
* @param array $options Additional options
* @return array Generated form data
*/
private function generate_with_openai($prompt, $options = []) {
$context = $options['conversation_context'] ?? [];
$form_type = $options['form_type'] ?? 'post';
$language = $options['language'] ?? 'English';

// Add language to context for system prompt
$context['language'] = $language;

$system_prompt = $this->get_system_prompt($context, $form_type);

// Get model-specific configuration
$model_config = $this->get_model_config('openai', $this->current_model);

$body = [
'model' => $this->current_model,
'messages' => [
['role' => 'system', 'content' => $system_prompt],
['role' => 'user', 'content' => $prompt]
]
];

// Set temperature based on model capabilities
if ($model_config['supports_custom_temperature']) {
$body['temperature'] = floatval($options['temperature'] ?? 0.7);
} else {
// Use fixed temperature for models that don't support custom temperature
$body['temperature'] = $model_config['temperature'] ?? 1.0;
}

// Set response format based on model capabilities
if ($model_config['supports_json_mode']) {
$body['response_format'] = ['type' => 'json_object'];
} else {
// For models that don't support JSON mode, add explicit instruction to system prompt
$system_prompt .= "\n\nIMPORTANT: You must respond with ONLY valid JSON. Do not include any explanatory text, markdown formatting, or code blocks. Return ONLY the JSON object.";
// Update the message payload with the modified system prompt
$body['messages'][0]['content'] = $system_prompt;
}

// Set token parameter based on model
if ($model_config['token_location'] === 'body') {
// GPT-5 needs significantly more tokens for reasoning + output
if (strpos($this->current_model, 'gpt-5') === 0) {
$body[$model_config['token_param']] = intval($options['max_tokens'] ?? 65536);
} else {
$body[$model_config['token_param']] = intval($options['max_tokens'] ?? 2000);
}
}

$args = [
'method' => 'POST',
'headers' => [
'Authorization' => 'Bearer ' . $this->api_key,
'Content-Type' => 'application/json'
],
'body' => json_encode($body),
'timeout' => 120
];

$response = wp_safe_remote_request($this->provider_configs['openai']['endpoint'], $args);

if (is_wp_error($response)) {
$error_message = $response->get_error_message();

// Check for specific timeout errors
if (strpos($error_message, 'timeout') !== false || strpos($error_message, 'timed out') !== false) {
throw new \Exception('OpenAI API request timed out. Please try again later.');
}

throw new \Exception('OpenAI API request failed: ' . $error_message);
}

$status_code = wp_remote_retrieve_response_code($response);
if ($status_code !== 200) {
$error_body = wp_remote_retrieve_body($response);
throw new \Exception("OpenAI API returned HTTP {$status_code}: {$error_body}");
}

$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);

// Validate JSON response
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('Invalid JSON response from AI provider: ' . json_last_error_msg());
}

if (isset($data['error'])) {
throw new \Exception('OpenAI API Error: ' . $data['error']['message']);
}

if (!isset($data['choices'][0]['message']['content'])) {
throw new \Exception('Invalid OpenAI response format. Response: ' . json_encode($data));
}

$content = $data['choices'][0]['message']['content'];

// Check for empty response
if (empty($content)) {
// Return error response instead of fallback
return [
'success' => false,
'error' => true,
'message' => 'AI model returned empty response. Please try again.',
'provider' => 'openai',
'model' => $this->current_model
];
}

// Clean and extract JSON from the response
$json_content = trim($content);

// Remove any markdown code blocks if present
$json_content = preg_replace('/^```(?:json)?\s*|\s*```$/m', '', $json_content);

// Remove any text before the first { or after the last }
$json_content = preg_replace('/^[^{]*/', '', $json_content);
$json_content = preg_replace('/[^}]*$/', '', $json_content);

// Try to find the JSON object (handle nested braces properly)
$start = strpos($json_content, '{');
$end = strrpos($json_content, '}');

if ($start !== false && $end !== false && $end > $start) {
$json_content = substr($json_content, $start, $end - $start + 1);
}

// Attempt to decode JSON
$ai_response = json_decode($json_content, true);

if (json_last_error() !== JSON_ERROR_NONE) {
return [
'success' => false,
'error' => true,
'message' => 'Unable to generate form. Please try again or rephrase your request.',
'provider' => 'openai',
'model' => $this->current_model
];
}

// Check for error response from AI
if ( ! empty( $ai_response['error'] ) ) {
return [
'success' => false,
'error' => true,
'message' => $ai_response['message'] ?? 'AI returned an error response',
'provider' => 'openai',
'model' => $this->current_model
];
}

// Build complete form from minimal AI response
$form_data = Form_Builder::build_form( $ai_response );

// Check if form building failed
if ( ! empty( $form_data['error'] ) ) {
return [
'success' => false,
'error' => true,
'message' => $form_data['message'] ?? 'Failed to build form structure',
'provider' => 'openai',
'model' => $this->current_model
];
}

// Add metadata with better uniqueness
$timestamp = microtime(true);
$random = bin2hex(random_bytes(5));
$form_data['session_id'] = $options['session_id'] ?? 'wpuf_ai_session_' . $timestamp . '_' . $random;
$form_data['response_id'] = 'openai_resp_' . $timestamp . '_' . $random;
$form_data['provider'] = 'openai';
$form_data['model'] = $this->current_model;
$form_data['generated_at'] = current_time('mysql');
$form_data['success'] = true;

return $form_data;
}

/**
* Generate form using Anthropic Claude
*
* @since 4.2.1
*
* @param string $prompt User prompt
* @param array $options Additional options
* @return array Generated form data
*/
private function generate_with_anthropic($prompt, $options = []) {
$context = $options['conversation_context'] ?? [];
$form_type = $options['form_type'] ?? 'post';
$language = $options['language'] ?? 'English';

// Add language to context for system prompt
$context['language'] = $language;

$system_prompt = $this->get_system_prompt($context, $form_type);

// Get model-specific configuration
$model_config = $this->get_model_config('anthropic', $this->current_model);

$body = [
'model' => $this->current_model,
'system' => $system_prompt,
'messages' => [
['role' => 'user', 'content' => $prompt]
]
];

// Set temperature based on model capabilities
if ($model_config['supports_custom_temperature']) {
$body['temperature'] = floatval($options['temperature'] ?? 0.7);
} else {
// Use fixed temperature for models that don't support custom temperature
$body['temperature'] = $model_config['temperature'] ?? 1.0;
}

// Set token parameter based on model
if ($model_config['token_location'] === 'body') {
// GPT-5 needs significantly more tokens for reasoning + output
if (strpos($this->current_model, 'gpt-5') === 0) {
$body[$model_config['token_param']] = intval($options['max_tokens'] ?? 65536);
} else {
$body[$model_config['token_param']] = intval($options['max_tokens'] ?? 2000);
}
}

$args = [
'method' => 'POST',
'headers' => [
'x-api-key' => $this->api_key,
'anthropic-version' => '2023-06-01',
'Content-Type' => 'application/json'
],
'body' => json_encode($body),
'timeout' => 120
];

$response = wp_safe_remote_request($this->provider_configs['anthropic']['endpoint'], $args);

if (is_wp_error($response)) {
throw new \Exception('Anthropic API request failed: ' . $response->get_error_message());
}

$status_code = wp_remote_retrieve_response_code($response);
if ($status_code !== 200) {
$error_body = wp_remote_retrieve_body($response);
throw new \Exception("Anthropic API returned HTTP {$status_code}: {$error_body}");
}

$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);

// Validate JSON response
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('Invalid JSON response from Anthropic API: ' . json_last_error_msg());
}

if (isset($data['error'])) {
throw new \Exception('Anthropic API Error: ' . $data['error']['message']);
}

if (!isset($data['content'][0]['text'])) {
throw new \Exception('Invalid Anthropic response format');
}

$content = $data['content'][0]['text'];

// Clean and extract JSON from the response (Claude may include explanatory text)
$json_content = trim($content);
$json_content = preg_replace('/^```(?:json)?\s*|\s*```$/m', '', $json_content);
$json_content = preg_replace('/^[^{]*/', '', $json_content);
$json_content = preg_replace('/[^}]*$/', '', $json_content);

$start = strpos($json_content, '{');
$end = strrpos($json_content, '}');
if ($start !== false && $end !== false && $end > $start) {
$json_content = substr($json_content, $start, $end - $start + 1);
}

// Decode JSON
$ai_response = json_decode($json_content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('Unable to generate form. Please try again or rephrase your request.');
}

// Check for error response from AI
if ( ! empty( $ai_response['error'] ) ) {
return [
'success' => false,
'error' => true,
'message' => $ai_response['message'] ?? 'AI returned an error response',
'provider' => 'anthropic',
'model' => $this->current_model
];
}

// Build complete form from minimal AI response
$form_data = Form_Builder::build_form( $ai_response );

// Check if form building failed
if ( ! empty( $form_data['error'] ) ) {
return [
'success' => false,
'error' => true,
'message' => $form_data['message'] ?? 'Failed to build form structure',
'provider' => 'anthropic',
'model' => $this->current_model
];
}

// Add metadata
$timestamp = microtime(true);
$random = bin2hex(random_bytes(5));
$form_data['session_id'] = $options['session_id'] ?? 'wpuf_ai_session_' . $timestamp . '_' . $random;
$form_data['response_id'] = 'anthropic_resp_' . $timestamp . '_' . $random;
$form_data['provider'] = 'anthropic';
$form_data['model'] = $this->current_model;
$form_data['generated_at'] = current_time('mysql');
$form_data['success'] = true;

return $form_data;
}


/**
* Generate form using Google Gemini
*
* @since 4.2.1
*
* @param string $prompt User prompt
* @param array $options Additional options
* @return array Generated form data
*/
private function generate_with_google($prompt, $options = []) {
$context = $options['conversation_context'] ?? [];
$form_type = $options['form_type'] ?? 'post';
$language = $options['language'] ?? 'English';

// Add language to context for system prompt
$context['language'] = $language;

$system_prompt = $this->get_system_prompt($context, $form_type);

// Get model-specific configuration
$model_config = $this->get_model_config('google', $this->current_model);

// Build endpoint with model
$endpoint = str_replace('{model}', $this->current_model, $this->provider_configs['google']['endpoint']);
$endpoint .= '?key=' . $this->api_key;

$body = [
'contents' => [
[
'parts' => [
['text' => $system_prompt . "\n\nUser request: " . $prompt]
]
]
],
'generationConfig' => []
];

// Set temperature based on model capabilities
if ($model_config['supports_custom_temperature']) {
$body['generationConfig']['temperature'] = floatval($options['temperature'] ?? 0.7);
} else {
// Use fixed temperature for models that don't support custom temperature
$body['generationConfig']['temperature'] = $model_config['temperature'] ?? 1.0;
}

// Set response format based on model capabilities
if ($model_config['supports_json_mode']) {
$body['generationConfig']['responseMimeType'] = 'application/json';
}

// Set token parameter based on model
if ($model_config['token_location'] === 'generationConfig') {
$body['generationConfig'][$model_config['token_param']] = intval($options['max_tokens'] ?? 2000);
}

$args = [
'method' => 'POST',
'headers' => [
'Content-Type' => 'application/json'
],
'body' => json_encode($body),
'timeout' => 120
];

$response = wp_safe_remote_request($endpoint, $args);

if (is_wp_error($response)) {
throw new \Exception('Google API request failed: ' . $response->get_error_message());
}

$status_code = wp_remote_retrieve_response_code($response);
if ($status_code !== 200) {
$error_body = wp_remote_retrieve_body($response);
throw new \Exception("Google API returned HTTP {$status_code}: {$error_body}");
}

$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);

// Validate JSON response
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('Invalid JSON response from Google API: ' . json_last_error_msg());
}

if (isset($data['error'])) {
throw new \Exception('Google API Error: ' . ($data['error']['message'] ?? 'Unknown error'));
}

if (!isset($data['candidates'][0]['content']['parts'][0]['text'])) {
throw new \Exception('Invalid Google response format');
}

$content = $data['candidates'][0]['content']['parts'][0]['text'];

// Clean and extract JSON from content
$json_content = trim($content);

// Remove any markdown code blocks if present
$json_content = preg_replace('/^```(?:json)?\s*|\s*```$/m', '', $json_content);

// Remove any text before the first { or after the last }
$json_content = preg_replace('/^[^{]*/', '', $json_content);
$json_content = preg_replace('/[^}]*$/', '', $json_content);

// Try to find the JSON object (handle nested braces properly)
$start = strpos($json_content, '{');
$end = strrpos($json_content, '}');

if ($start !== false && $end !== false && $end > $start) {
$json_content = substr($json_content, $start, $end - $start + 1);
}

// Attempt to decode JSON
$ai_response = json_decode($json_content, true);

if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('Unable to generate form. Please try again or rephrase your request.');
}

// Check for error response from AI
if ( ! empty( $ai_response['error'] ) ) {
return [
'success' => false,
'error' => true,
'message' => $ai_response['message'] ?? 'AI returned an error response',
'provider' => 'google',
'model' => $this->current_model
];
}

// Build complete form from minimal AI response
$form_data = Form_Builder::build_form( $ai_response );

// Check if form building failed
if ( ! empty( $form_data['error'] ) ) {
return [
'success' => false,
'error' => true,
'message' => $form_data['message'] ?? 'Failed to build form structure',
'provider' => 'google',
'model' => $this->current_model
];
}

// Add metadata with better uniqueness
$timestamp = microtime(true);
$random = bin2hex(random_bytes(5));
$form_data['session_id'] = $options['session_id'] ?? 'wpuf_ai_session_' . $timestamp . '_' . $random;
$form_data['response_id'] = 'google_resp_' . $timestamp . '_' . $random;
$form_data['provider'] = 'google';
$form_data['model'] = $this->current_model;
$form_data['generated_at'] = current_time('mysql');
$form_data['success'] = true;

return $form_data;
}

/**
* Get system prompt for AI form generation
*
* @since 4.2.1
*
* @param array $context Conversation context
* @param string $form_type Form type ('post' or 'profile')
* @return string System prompt
*/
private function get_system_prompt($context = [], $form_type = 'post') {
// Determine which prompt file to use based on form type
if ( 'profile' === $form_type || 'registration' === $form_type ) {
// Registration/Profile form prompt - USE MINIMAL REGISTRATION PROMPT
$prompt_file = WPUF_ROOT . '/includes/AI/wpuf-ai-minimal-prompt-registration.md';
} else {
// Post form prompt - USE MINIMAL PROMPT
$prompt_file = WPUF_ROOT . '/includes/AI/wpuf-ai-minimal-prompt.md';
}

// Check if file exists
if ( ! file_exists( $prompt_file ) ) {
throw new \Exception( 'System prompt file not found: ' . $prompt_file );
}

// Load the prompt file
$system_prompt = file_get_contents( $prompt_file );

// Add form type context (informational, not restrictive)
$system_prompt .= "\n\n## FORM TYPE CONTEXT\n";
if ( 'profile' === $form_type || 'registration' === $form_type ) {
$system_prompt .= "You are working with a REGISTRATION/PROFILE form.\n";
$system_prompt .= "- Use registration/profile fields: user_email, user_login, password, first_name, last_name, biography, user_avatar, social fields, phone_field, address_field, dropdown_field, radio_field, checkbox_field, etc.\n";
$system_prompt .= "- Custom fields like dropdown, radio, checkbox, text fields are fully supported for additional profile information\n";
$system_prompt .= "- Focus on helping users collect user registration and profile data\n";
} else {
$system_prompt .= "You are working with a POST submission form.\n";
$system_prompt .= "- Use post fields: post_title, post_content, post_excerpt, featured_image, taxonomy, custom fields, etc.\n";
$system_prompt .= "- Focus on helping users collect content submission data\n";
}

// Add language context if provided or extract from user message
$target_language = $context['language'] ?? 'English';

// Extract language from user message if they're requesting conversion/translation
if ( ! empty( $context['chat_history'] ) && is_array( $context['chat_history'] ) && count( $context['chat_history'] ) > 0 ) {
$last_message = end( $context['chat_history'] );
$last_user_message = $last_message['content'] ?? '';

// Detect language conversion/translation requests
if ( preg_match( '/(?:convert|translate|change|make).*?(?:to|in|into)\s+(\w+)/i', $last_user_message, $matches ) ) {
$detected_language = ucfirst( strtolower( $matches[1] ) );
$target_language = $detected_language;
}
}

if ( ! empty( $target_language ) && $target_language !== 'English' ) {
$system_prompt .= "\n\n## TARGET LANGUAGE\n";
$system_prompt .= "**CRITICAL: The user has selected '{$target_language}' as their target language.**\n";
$system_prompt .= "- Generate ALL field labels in {$target_language}\n";
$system_prompt .= "- Generate ALL field placeholders in {$target_language}\n";
$system_prompt .= "- Generate ALL field help text in {$target_language}\n";
$system_prompt .= "- Generate ALL dropdown/radio/checkbox options in {$target_language}\n";
$system_prompt .= "- Generate form_title and form_description in {$target_language}\n";
$system_prompt .= "- When adding new fields, use {$target_language} for all text content\n";
$system_prompt .= "- Even if the user's message is in English, generate field content in {$target_language}\n";
$system_prompt .= "- This is a language conversion request - update ALL existing field labels to {$target_language}\n\n";
}

// Add conversation context if provided
if ( ! empty( $context ) ) {
$system_prompt .= "\n\n## CURRENT CONVERSATION CONTEXT\n";

// Safely extract last user message
$last_user_message = '';
if ( isset( $context['chat_history'] ) && is_array( $context['chat_history'] ) && count( $context['chat_history'] ) > 0 ) {
$last_message = end( $context['chat_history'] );
$last_user_message = $last_message['content'] ?? '';
}

// Determine modification intent
$modification_requested = false;
if ( isset( $context['modification_requested'] ) ) {
$modification_requested = (bool) $context['modification_requested'];
} else {
$modification_keywords = [ 'edit', 'modify', 'update', 'change', 'add', 'remove', 'delete', 'replace', 'convert', 'translate', 'make' ];
foreach ( $modification_keywords as $keyword ) {
if ( false !== stripos( $last_user_message, $keyword ) ) {
$modification_requested = true;
break;
}
}
}

// Build MINIMAL context - only send template + label, not full structures
$context_for_ai = [
'session_id' => $context['session_id'] ?? '',
'last_user_message' => $last_user_message,
'modification_requested' => $modification_requested,
'form_type' => $form_type, // Pass form type to AI for validation
];

// Include MINIMAL current fields (template + label + custom props only)
if ( ! empty( $context['current_form'] ) ) {
$minimal_fields = Form_Builder::extract_minimal_fields( $context['current_form'] );
if ( ! empty( $minimal_fields ) ) {
$context_for_ai['current_fields'] = $minimal_fields;
$context_for_ai['form_title'] = $context['current_form']['form_title'] ?? '';
$context_for_ai['form_description'] = $context['current_form']['form_description'] ?? '';
}
}

$system_prompt .= json_encode( $context_for_ai, JSON_PRETTY_PRINT );

// Add specific instruction for modifications
$system_prompt .= "\n\n## MODIFICATION INSTRUCTION\n";
if ( $modification_requested ) {
$system_prompt .= "The user wants to MODIFY the existing form. You MUST:\n";
$system_prompt .= "1. Return ALL existing fields from current_fields array (keep template + label + custom props)\n";
$system_prompt .= "2. Apply the requested modification (add/remove/edit specific fields)\n";
$system_prompt .= "3. Return COMPLETE field list with all fields in the 'fields' array\n";
$system_prompt .= "4. Include form_title and form_description from context\n\n";
$system_prompt .= "### Modification Types:\n";
$system_prompt .= "- **Add field**: Return existing fields + new field\n";
$system_prompt .= "- **Remove field**: Return existing fields without the specified field\n";
$system_prompt .= "- **Edit field**: Update the field's properties (label, required, placeholder, etc.)\n";
$system_prompt .= "- **Change field type**: Replace the field with a new template (e.g., checkbox_field → dropdown_field)\n";
$system_prompt .= " Example: 'change skills checkbox to dropdown' → Replace checkbox_field template with dropdown_field template\n\n";
$system_prompt .= "Examples:\n";
$system_prompt .= "- 'add email field' with 3 existing fields → return ALL 4 fields\n";
$system_prompt .= "- 'change skills from checkbox to dropdown' → return all fields with skills field having template: 'dropdown_field' instead of 'checkbox_field'\n";
} else {
$system_prompt .= "The user is asking a question. Return an error response with helpful message.";
}
}

return $system_prompt;
}


/**
* Get available providers
*
* @since 4.2.1
*
* @return array Provider configurations
*/
public function get_providers() {
return $this->provider_configs;
}

/**
* Get current provider
*
* @since 4.2.1
*
* @return string Current provider
*/
public function get_current_provider() {
return $this->current_provider;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clamp temperature server‑side and address minor coding‑standards issues

  • You correctly default temperature from wpuf_ai settings, but it’s only floatval()’d here. Consider clamping to [0.0, 1.0] at this call site as well (not just in the settings UI) so any malformed stored value can’t leak through to the provider APIs.
  • PHPCS hint: change get_option('wpuf_ai', []); to WordPress style with spaces: get_option( 'wpuf_ai', [] );.

Also applies to: 113-116

🧰 Tools
🪛 GitHub Check: Run PHPCS inspection

[failure] 99-99:
Expected 1 spaces before closing parenthesis; 0 found


[failure] 99-99:
Expected 1 spaces after opening parenthesis; 0 found

🤖 Prompt for AI Agents
In includes/AI/FormGenerator.php around lines 98-100 (and similarly at 113-116)
the call to get_option should use WordPress spacing and the retrieved
temperature must be clamped server-side; change get_option('wpuf_ai', []); to
get_option( 'wpuf_ai', [] ); then when reading the temperature, cast to float
and clamp it into the range 0.0–1.0 (e.g. use min(max(floatval(...), 0.0), 1.0))
before using or passing it to provider APIs so malformed stored values cannot
leak through.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
includes/functions/settings-options.php (1)

1213-1217: Fix PHPCS spacing issues around rest_url() / wp_create_nonce()

The pipeline PHPCS error (“Expected 1 space after opening parenthesis; 0 found”) is likely coming from these calls without WordPress‑style spacing:

url: '<?php echo esc_url(rest_url('wpuf/v1/ai-form-builder/refresh-google-models')); ?>',
xhr.setRequestHeader('X-WP-Nonce', '<?php echo wp_create_nonce('wp_rest'); ?>');

Apply the standard spacing to satisfy PHPCS:

-url: '<?php echo esc_url(rest_url('wpuf/v1/ai-form-builder/refresh-google-models')); ?>',
+url: '<?php echo esc_url( rest_url( 'wpuf/v1/ai-form-builder/refresh-google-models' ) ); ?>',

- xhr.setRequestHeader('X-WP-Nonce', '<?php echo wp_create_nonce('wp_rest'); ?>');
+ xhr.setRequestHeader('X-WP-Nonce', '<?php echo wp_create_nonce( 'wp_rest' ); ?>');

and similarly for the other rest_url() / wp_create_nonce() usages in this file.

Also applies to: 1257-1261, 1292-1296

🧹 Nitpick comments (3)
includes/functions/settings-options.php (3)

738-753: Unify API key description and actually use the translatable desc string

The field’s desc already uses sprintf() and translator comments, but wpuf_ai_api_key_field() renders its own hard‑coded, non‑translatable description instead and ignores $args['desc']. That duplicates strings and makes future text changes harder.

Consider reusing the desc passed in $args inside the callback, e.g.:

<?php if ( ! empty( $args['desc'] ) ) : ?>
    <p class="description">
        <?php echo wp_kses_post( $args['desc'] ); ?>
    </p>
<?php endif; ?>

and then drop the manual description block here. This also keeps the previous sprintf() i18n fix effective.

Also applies to: 923-931


854-906: API key masking and provider switching look solid; minor UX polish possible

The masking logic, per‑provider hidden fields, and the “Change” flow are implemented cleanly, and updateModelFieldState() correctly gates the model dropdown on key presence/length.

Two small UX tweaks you might consider (non‑blocking):

  • On initial page load, also disable/enable the “Test Connection” button based on the current key (similar to how you initialize the model field), so it doesn’t look clickable when there is clearly no key.
  • On page load, you could also call updateApiKeyDisplay(initialProvider); and updateApiKeyLink(initialProvider); to ensure the visible state and help link are fully in sync with the saved provider without waiting for a change event.

These are minor and don’t affect correctness.

Also applies to: 961-1040


1064-1079: Reuse the field desc and the $args parameter in wpuf_ai_temperature_field

The temperature field currently hard‑codes its description text in the callback while the field definition also sets a desc with the same sentence, and $args is reported as unused by PHPMD.

To avoid duplicated strings and silence the unused‑parameter warning, consider:

 function wpuf_ai_temperature_field( $args ) {
     $settings = get_option( 'wpuf_ai', [] );
     $value = isset( $settings['temperature'] ) ? floatval( $settings['temperature'] ) : 0.7;

     // Ensure value is within valid range
     $value = max( 0.0, min( 1.0, $value ) );
     ?>
     <input type="number"
            id="wpuf_ai_temperature"
            name="wpuf_ai[temperature]"
            class="regular-text wpuf-ai-temperature"
            value="<?php echo esc_attr( $value ); ?>"
            min="0"
            max="1"
            step="0.1"
            required>
-    <p class="description">
-        <?php esc_html_e( 'Controls randomness in responses. Lower values (0.1-0.3) are more focused and deterministic. Higher values (0.7-1.0) are more creative and varied.', 'wp-user-frontend' ); ?>
-    </p>
+    <?php if ( ! empty( $args['desc'] ) ) : ?>
+        <p class="description">
+            <?php echo wp_kses_post( $args['desc'] ); ?>
+        </p>
+    <?php endif; ?>
     <?php
 }

This keeps the single source of truth for the help text in the field definition and uses $args meaningfully.

Also applies to: 1357-1383

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0cfd6a5 and 59fc5bb.

📒 Files selected for processing (1)
  • includes/functions/settings-options.php (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
includes/functions/settings-options.php (1)
Lib/WeDevs_Settings_API.php (1)
  • get_option (649-658)
🪛 GitHub Actions: Inspections
includes/functions/settings-options.php

[error] 1-1: PHPCS: Expected 1 space after opening parenthesis; 0 found. Command: vendor/bin/phpcs includes/AI/FormGenerator.php includes/AI/RestController.php includes/functions/settings-options.php -q --report=checkstyle | cs2pr --graceful-warnings

🪛 PHPMD (2.15.0)
includes/functions/settings-options.php

1362-1362: Avoid unused parameters such as '$args'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (1)
includes/functions/settings-options.php (1)

1334-1353: CSS/UX for spinner and status area looks good

The custom @keyframes spin, .dashicons.spin, and .wpuf-ai-api-key-wrapper styles cleanly support the spinner behavior and layout without leaking outside this field’s scope. No issues from a functionality standpoint.

$test_prompt = 'Generate a simple contact form with name and email fields.';
$result = $this->generate_form($test_prompt, ['max_tokens' => 500]);
// Reload settings to get latest data
$this->load_settings();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can call load_settings() only when $api_key =, $provider or $model is not provided. That way we can reduce one DB call

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done @sapayth vai

/**
* Test provider API with minimal request
*
* @since 4.2.1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use WPUF_SINCE

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done @sapayth vai

@sapayth sapayth added needs: author reply This PR needs author feedback or code changes and removed needs: dev review This PR needs review by a developer labels Jan 1, 2026
arifulhoque7 and others added 2 commits January 1, 2026 15:14
Optimized settings reload in FormGenerator::test_connection to reduce unnecessary DB calls. Added escapeHtml helper and applied it to status messages in settings-options.php to prevent XSS vulnerabilities when displaying API responses.
@arifulhoque7 arifulhoque7 requested a review from sapayth January 1, 2026 09:20
@arifulhoque7 arifulhoque7 removed the needs: author reply This PR needs author feedback or code changes label Jan 1, 2026
@arifulhoque7 arifulhoque7 added the needs: dev review This PR needs review by a developer label Jan 1, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
includes/AI/FormGenerator.php (2)

863-873: Use actual version constant instead of placeholder

The @since tag on line 866 references a placeholder. Based on the past review comment from sapayth, use the WPUF_SINCE constant instead:

     /**
      * Test connection to provider with optional overrides
      *
-     * @since 4.2.1
+     * @since WPUF_SINCE

98-116: Apply WordPress coding standards and clamp temperature server-side

Two issues remain unresolved from previous reviews:

  1. Spacing: Line 99 should use WordPress coding standards: get_option( 'wpuf_ai', [] ); (spaces after opening and before closing parenthesis).

  2. Temperature clamping: Line 115 retrieves temperature but only applies floatval(). Any malformed stored value outside [0.0, 1.0] will leak through to provider APIs. Clamp it:

$options['temperature'] = max( 0.0, min( 1.0, floatval( $settings['temperature'] ) ) );
🔎 Proposed fix
         // Load settings for defaults
-        $settings = get_option('wpuf_ai', []);
+        $settings = get_option( 'wpuf_ai', [] );

         // Apply per-request overrides if provided
         if ( isset($options['provider']) && ! empty($options['provider']) ) {
@@ -112,7 +112,7 @@

         // Set default temperature from settings if not provided in options
         if ( ! isset($options['temperature']) ) {
-            $options['temperature'] = isset($settings['temperature']) ? floatval($settings['temperature']) : 0.7;
+            $options['temperature'] = isset( $settings['temperature'] ) ? max( 0.0, min( 1.0, floatval( $settings['temperature'] ) ) ) : 0.7;
         }

As per coding guidelines and past review comments.

🧹 Nitpick comments (2)
includes/AI/FormGenerator.php (2)

954-997: Sanitize API error messages before returning to REST endpoint

The error messages extracted from provider API responses (lines 990, 1036, 1086) are passed directly to the REST response without sanitization. While the frontend now uses escapeHtml() to prevent XSS, defense-in-depth suggests sanitizing on the backend as well.

Apply sanitize_text_field() to error messages before returning:

$error_message = isset( $body['error']['message'] ) ? sanitize_text_field( $body['error']['message'] ) : __( 'Connection failed', 'wp-user-frontend' );

Also applies to lines 1036 (test_anthropic_connection) and 1086 (test_google_connection).


964-968: Format multi-item associative array per PHPCS standards

Line 966 contains a multi-item associative array that should be formatted with each value on a new line for consistency with project coding standards:

         'body'    => wp_json_encode( [
             'model'      => $this->current_model,
-            'messages'   => [ [ 'role' => 'user', 'content' => 'Hi' ] ],
+            'messages'   => [
+                [
+                    'role'    => 'user',
+                    'content' => 'Hi',
+                ],
+            ],
             'max_tokens' => 5,
         ] ),

Also applies to lines 1012 (test_anthropic_connection) and 1059 (test_google_connection).

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59fc5bb and 6346439.

📒 Files selected for processing (2)
  • includes/AI/FormGenerator.php
  • includes/functions/settings-options.php
🧰 Additional context used
🧬 Code graph analysis (2)
includes/AI/FormGenerator.php (2)
Lib/WeDevs_Settings_API.php (1)
  • get_option (649-658)
includes/AI/RestController.php (1)
  • test_connection (405-423)
includes/functions/settings-options.php (1)
Lib/WeDevs_Settings_API.php (1)
  • get_option (649-658)
🪛 GitHub Check: Run PHPCS inspection
includes/AI/FormGenerator.php

[failure] 99-99:
Expected 1 spaces before closing parenthesis; 0 found


[failure] 99-99:
Expected 1 spaces after opening parenthesis; 0 found


[warning] 966-966:
When a multi-item array uses associative keys, each value should start on a new line.

🪛 PHPMD (2.15.0)
includes/functions/settings-options.php

1367-1367: Avoid unused parameters such as '$args'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (5)
includes/functions/settings-options.php (5)

738-753: LGTM!

The API key field configuration correctly uses sprintf() with translatable strings and esc_url() for the help links. This addresses the past review feedback.


854-866: LGTM!

The API key masking logic correctly shows the first 4 and last 4 characters while hiding the middle, with appropriate handling for short keys (≤8 characters). This prevents key exposure while allowing users to verify which key is configured.


1084-1168: LGTM!

The JavaScript correctly implements XSS prevention:

  • The escapeHtml() helper (lines 1084-1087) uses jQuery's .text() method to safely escape HTML entities.
  • Applied consistently in success handler (line 1137) and both error handlers (lines 1149, 1164).

This addresses the previous XSS vulnerability concern.


1367-1388: LGTM!

The temperature field implementation is solid:

  • Server-side clamping (line 1372) ensures values stay within [0.0, 1.0].
  • HTML5 attributes (min, max, step, required) provide client-side validation.
  • Clear description guides users on appropriate values.

Note: PHPMD's warning about unused $args parameter is a false positive—WordPress Settings API callbacks require this signature.


1064-1079: Excellent UX for temperature validation

The temperature field validation provides clear visual feedback:

  • Leverages HTML5's checkValidity() for standards-based validation.
  • Red border for invalid values, green for valid.
  • Auto-clears success indicator after 1 second to avoid visual clutter.

This is a well-implemented user experience pattern.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs: dev review This PR needs review by a developer QA Approved This PR is approved by the QA team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enhancement] Need test connection, temperature and token limit for AI settings

3 participants