-
Notifications
You must be signed in to change notification settings - Fork 154
feat: enhance AI settings with temperature control, API key masking, and connection testing #1758
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
feat: enhance AI settings with temperature control, API key masking, and connection testing #1758
Conversation
…and connection testing
WalkthroughAdds 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
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. Comment |
There was a problem hiding this 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:
- Reloads settings to get latest data
- Stores original values before overriding
- Validates API key presence
- Restores original values after testing
- Handles exceptions with proper error response
However, the
catchblock 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
📒 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-Nonceheader- Passes provider, API key, and model for testing
1265-1285: Temperature field implementation is correct; unused parameter is acceptable.The static analysis flags
$argsas 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 ascurrent_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:
- The request includes sensitive API keys that shouldn't be in URL/logs
- The operation has side effects (makes external API calls)
- Parameters are properly typed and sanitized
The
sanitize_callbackforapi_keyusessanitize_text_fieldwhich 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:
- Loads settings with
get_option('wpuf_ai', [])- Only sets temperature if not already provided in options
- 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).
|
Token limit configuration is missing @arifulhoque7 vai |
Token limit configuration can cause broken JSON in the form builder, thus we can't implement it here @Rubaiyat-E-Mohammad vai |
There was a problem hiding this 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' ), |
There was a problem hiding this 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.
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.
done @sapayth vai |
There was a problem hiding this 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, whiletest_google_connection()correctly uses thex-goog-api-keyheader. 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_keyfrom the URL and instead includex-goog-api-keyin theheadersarray, mirroringtest_google_connection().
🧹 Nitpick comments (3)
includes/AI/FormGenerator.php (2)
873-923: Improve robustness of state restoration intest_connectionThe 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_keyare overridden, the catch block returns an error but never restores the originals, unlikegenerate_form().Wrapping restoration in a
finally-style pattern (or duplicating restoration in the catch) would make this more future‑proof and consistent withgenerate_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$argsand server‑side clamping
- The temperature input is correctly configured as an HTML5
<input type="number">withmin="0",max="1", andstep="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 inFormGenerator::generate_form().The
$argsparameter 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$_argsor 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
📒 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_providerand 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 UXThe new
api_key_currentcallback field andtemperaturecallback field are wired correctly (names, defaults, callbacks, descriptions), and the order (provider → key → model → temperature) matches the PR objective.
766-819: FixTest Connectionbehavior for already‑saved keys and align model gatingThe 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_fieldis empty and hidden.updateModelFieldState()is initialized withinitialApiKey = $('#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.
| // 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clamp temperature server‑side and address minor coding‑standards issues
- You correctly default
temperaturefromwpuf_aisettings, but it’s onlyfloatval()’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.
There was a problem hiding this 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 aroundrest_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 translatabledescstringThe field’s
descalready usessprintf()and translator comments, butwpuf_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
descpassed in$argsinside 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 possibleThe 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);andupdateApiKeyLink(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 fielddescand the$argsparameter inwpuf_ai_temperature_fieldThe temperature field currently hard‑codes its description text in the callback while the field definition also sets a
descwith the same sentence, and$argsis 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
$argsmeaningfully.Also applies to: 1357-1383
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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 goodThe custom
@keyframes spin,.dashicons.spin, and.wpuf-ai-api-key-wrapperstyles cleanly support the spinner behavior and layout without leaking outside this field’s scope. No issues from a functionality standpoint.
includes/AI/FormGenerator.php
Outdated
| $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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done @sapayth vai
includes/AI/FormGenerator.php
Outdated
| /** | ||
| * Test provider API with minimal request | ||
| * | ||
| * @since 4.2.1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use WPUF_SINCE
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done @sapayth vai
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.
There was a problem hiding this 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 placeholderThe
@sincetag on line 866 references a placeholder. Based on the past review comment from sapayth, use theWPUF_SINCEconstant 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-sideTwo issues remain unresolved from previous reviews:
Spacing: Line 99 should use WordPress coding standards:
get_option( 'wpuf_ai', [] );(spaces after opening and before closing parenthesis).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 endpointThe 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 standardsLine 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
📒 Files selected for processing (2)
includes/AI/FormGenerator.phpincludes/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 andesc_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
$argsparameter is a false positive—WordPress Settings API callbacks require this signature.
1064-1079: Excellent UX for temperature validationThe 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.


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)
wpuf_ai[temperature]and used in all AI API callsFiles Modified:
includes/functions/settings-options.php(lines 666-673, 1246-1267)includes/AI/FormGenerator.php(lines 113-116)2. Masked API Key Display
AIza**********************xyz)Files Modified:
includes/functions/settings-options.php(lines 760-788, 867-900)3. Test Connection Button
Implementation:
Files Modified:
includes/AI/FormGenerator.php(lines 863-1074) - Addedtest_connection()and provider-specific test methodsincludes/AI/RestController.php(lines 125-146, 405-423) - Added REST endpoint with parametersincludes/functions/settings-options.php(lines 787-806, 984-1066) - Frontend AJAX integration4. Reorganized Field Order
New logical workflow order:
Files Modified:
includes/functions/settings-options.php(lines 640-674)Technical Details
Temperature Handling:
wpuf_ai['temperature']option0.7(balanced creativity)max(0.0, min(1.0, $value))$options['temperature']Test Connection Flow:
/wpuf/v1/ai-form-builder/testwith JSON payloadSecurity:
sanitize_text_field()check_permission()for test endpointTest Plan
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
✏️ Tip: You can customize this high-level summary in your review settings.