From 13a91163bd32f86d51dbb23457f18afaf0c4bdc2 Mon Sep 17 00:00:00 2001 From: Ariful Hoque Date: Tue, 25 Nov 2025 12:40:15 +0600 Subject: [PATCH 1/2] Add AI-powered field options generator for dropdown, radio, and checkbox fields --- .../js/components/field-option-data/index.js | 106 ++++- .../components/field-option-data/template.php | 111 ++++- assets/js-templates/form-components.php | 111 ++++- assets/js/wpuf-form-builder-components.js | 106 ++++- includes/AI/FormGenerator.php | 386 ++++++++++++++++++ includes/AI/RestController.php | 160 ++++++++ includes/Admin/Forms/Admin_Form_Builder.php | 11 + src/css/admin/form-builder.css | 36 ++ 8 files changed, 1023 insertions(+), 4 deletions(-) diff --git a/admin/form-builder/assets/js/components/field-option-data/index.js b/admin/form-builder/assets/js/components/field-option-data/index.js index 7212bf8bc..de24f79c2 100644 --- a/admin/form-builder/assets/js/components/field-option-data/index.js +++ b/admin/form-builder/assets/js/components/field-option-data/index.js @@ -16,7 +16,13 @@ Vue.component('field-option-data', { sync_value: true, options: [], selected: [], - display: !this.editing_form_field.hide_option_data // hide this field for the events calendar + display: !this.editing_form_field.hide_option_data, // hide this field for the events calendar + show_ai_modal: false, + show_ai_config_modal: false, + ai_prompt: '', + ai_loading: false, + ai_error: '', + ai_generated_options: [] }; }, @@ -27,6 +33,12 @@ Vue.component('field-option-data', { field_selected: function () { return this.editing_form_field.selected; + }, + + all_ai_selected: function () { + return this.ai_generated_options.length > 0 && this.ai_generated_options.every(function(opt) { + return opt.selected; + }); } }, @@ -97,6 +109,98 @@ Vue.component('field-option-data', { if (this.sync_value) { this.options[index].value = label.toLocaleLowerCase().replace( /\s/g, '_' ); } + }, + + open_ai_modal: function () { + // Check if AI is configured + if (!wpuf_form_builder.ai_configured) { + this.show_ai_config_modal = true; + return; + } + this.show_ai_modal = true; + this.ai_prompt = ''; + this.ai_error = ''; + this.ai_generated_options = []; + }, + + close_ai_config_modal: function () { + this.show_ai_config_modal = false; + }, + + go_to_ai_settings: function () { + window.location.href = wpuf_form_builder.ai_settings_url; + }, + + close_ai_modal: function () { + this.show_ai_modal = false; + this.ai_prompt = ''; + this.ai_error = ''; + this.ai_generated_options = []; + this.ai_loading = false; + }, + + generate_ai_options: function () { + var self = this; + + if (!this.ai_prompt.trim()) { + return; + } + + this.ai_loading = true; + this.ai_error = ''; + + var field_type = this.editing_form_field.template; + + wp.ajax.post('wpuf_ai_generate_field_options', { + prompt: this.ai_prompt, + field_type: field_type, + nonce: wpuf_form_builder.nonce + }).done(function(response) { + // wp.ajax.post returns data directly in response (not response.data) + // when using wp_send_json_success(['options' => $options]) + var options = response.options || (response.data && response.data.options) || []; + + if (options.length > 0) { + var mapped_options = options.map(function(opt) { + return { + label: opt.label || opt, + value: opt.value || opt, + selected: true + }; + }); + self.$set(self, 'ai_generated_options', mapped_options); + } else { + self.ai_error = response.message || (response.data && response.data.message) || self.i18n.something_went_wrong; + } + }).fail(function(error) { + self.ai_error = error.message || self.i18n.something_went_wrong; + }).always(function() { + self.ai_loading = false; + }); + }, + + select_all_ai_options: function () { + var select_state = !this.all_ai_selected; + this.ai_generated_options.forEach(function(opt) { + opt.selected = select_state; + }); + }, + + import_ai_options: function () { + var self = this; + var selected_options = this.ai_generated_options.filter(function(opt) { + return opt.selected; + }); + + selected_options.forEach(function(opt) { + self.options.push({ + label: opt.label, + value: opt.value, + id: self.get_random_id() + }); + }); + + this.close_ai_modal(); } }, diff --git a/admin/form-builder/assets/js/components/field-option-data/template.php b/admin/form-builder/assets/js/components/field-option-data/template.php index dd5b561e8..c9bdd33df 100644 --- a/admin/form-builder/assets/js/components/field-option-data/template.php +++ b/admin/form-builder/assets/js/components/field-option-data/template.php @@ -25,7 +25,21 @@ class="!wpuf-mr-2" />
- +
+ + +
+ + +
+
+
+

+ +
+
+ + +
{{ ai_error }}
+
+
+ + +
+
+ +
+
+
+ +
+
+ + +
+
+ +
+ + + + +
+ + +

+ +

+ + +

+ +

+ + +
+ + +
+
+
diff --git a/assets/js-templates/form-components.php b/assets/js-templates/form-components.php index 7001a5830..d1a8de691 100644 --- a/assets/js-templates/form-components.php +++ b/assets/js-templates/form-components.php @@ -410,7 +410,21 @@ class="!wpuf-mr-2" />
- +
+ + +
+ + +
+
+
+

+ +
+
+ + +
{{ ai_error }}
+
+
+ + +
+
+ +
+
+
+ +
+
+ + +
+
+ +
+ + + + +
+ + +

+ +

+ + +

+ +

+ + +
+ + +
+
+
diff --git a/assets/js/wpuf-form-builder-components.js b/assets/js/wpuf-form-builder-components.js index e223647e8..7c322fd9e 100644 --- a/assets/js/wpuf-form-builder-components.js +++ b/assets/js/wpuf-form-builder-components.js @@ -650,7 +650,13 @@ Vue.component('field-option-data', { sync_value: true, options: [], selected: [], - display: !this.editing_form_field.hide_option_data // hide this field for the events calendar + display: !this.editing_form_field.hide_option_data, // hide this field for the events calendar + show_ai_modal: false, + show_ai_config_modal: false, + ai_prompt: '', + ai_loading: false, + ai_error: '', + ai_generated_options: [] }; }, @@ -661,6 +667,12 @@ Vue.component('field-option-data', { field_selected: function () { return this.editing_form_field.selected; + }, + + all_ai_selected: function () { + return this.ai_generated_options.length > 0 && this.ai_generated_options.every(function(opt) { + return opt.selected; + }); } }, @@ -731,6 +743,98 @@ Vue.component('field-option-data', { if (this.sync_value) { this.options[index].value = label.toLocaleLowerCase().replace( /\s/g, '_' ); } + }, + + open_ai_modal: function () { + // Check if AI is configured + if (!wpuf_form_builder.ai_configured) { + this.show_ai_config_modal = true; + return; + } + this.show_ai_modal = true; + this.ai_prompt = ''; + this.ai_error = ''; + this.ai_generated_options = []; + }, + + close_ai_config_modal: function () { + this.show_ai_config_modal = false; + }, + + go_to_ai_settings: function () { + window.location.href = wpuf_form_builder.ai_settings_url; + }, + + close_ai_modal: function () { + this.show_ai_modal = false; + this.ai_prompt = ''; + this.ai_error = ''; + this.ai_generated_options = []; + this.ai_loading = false; + }, + + generate_ai_options: function () { + var self = this; + + if (!this.ai_prompt.trim()) { + return; + } + + this.ai_loading = true; + this.ai_error = ''; + + var field_type = this.editing_form_field.template; + + wp.ajax.post('wpuf_ai_generate_field_options', { + prompt: this.ai_prompt, + field_type: field_type, + nonce: wpuf_form_builder.nonce + }).done(function(response) { + // wp.ajax.post returns data directly in response (not response.data) + // when using wp_send_json_success(['options' => $options]) + var options = response.options || (response.data && response.data.options) || []; + + if (options.length > 0) { + var mapped_options = options.map(function(opt) { + return { + label: opt.label || opt, + value: opt.value || opt, + selected: true + }; + }); + self.$set(self, 'ai_generated_options', mapped_options); + } else { + self.ai_error = response.message || (response.data && response.data.message) || self.i18n.something_went_wrong; + } + }).fail(function(error) { + self.ai_error = error.message || self.i18n.something_went_wrong; + }).always(function() { + self.ai_loading = false; + }); + }, + + select_all_ai_options: function () { + var select_state = !this.all_ai_selected; + this.ai_generated_options.forEach(function(opt) { + opt.selected = select_state; + }); + }, + + import_ai_options: function () { + var self = this; + var selected_options = this.ai_generated_options.filter(function(opt) { + return opt.selected; + }); + + selected_options.forEach(function(opt) { + self.options.push({ + label: opt.label, + value: opt.value, + id: self.get_random_id() + }); + }); + + this.close_ai_modal(); } }, diff --git a/includes/AI/FormGenerator.php b/includes/AI/FormGenerator.php index b1118d750..49fc70ab1 100644 --- a/includes/AI/FormGenerator.php +++ b/includes/AI/FormGenerator.php @@ -887,4 +887,390 @@ public function test_connection() { ]; } } + + /** + * Generate field options using AI + * + * @since 4.2.2 + * + * @param string $prompt User prompt describing the options to generate + * @param array $options Additional options + * @return array Generated options + */ + public function generate_field_options($prompt, $options = []) { + try { + $field_type = $options['field_type'] ?? 'dropdown_field'; + $output_format = $options['output_format'] ?? 'one_per_line'; + $tone = $options['tone'] ?? 'casual'; + $max_options = $options['max_options'] ?? 20; + + // Build system prompt for option generation + $system_prompt = $this->get_field_options_system_prompt($field_type, $output_format, $tone, $max_options); + + // Store original provider settings + $original_provider = $this->current_provider; + $original_model = $this->current_model; + $original_api_key = $this->api_key; + + // Call AI provider + $ai_response = null; + switch ($this->current_provider) { + case 'openai': + $ai_response = $this->call_openai_for_options($system_prompt, $prompt, $options); + break; + + case 'anthropic': + $ai_response = $this->call_anthropic_for_options($system_prompt, $prompt, $options); + break; + + case 'google': + $ai_response = $this->call_google_for_options($system_prompt, $prompt, $options); + break; + + default: + throw new \Exception('Unsupported AI provider: ' . $this->current_provider); + } + + // Restore original provider settings + $this->current_provider = $original_provider; + $this->current_model = $original_model; + $this->api_key = $original_api_key; + + if (isset($ai_response['error']) && $ai_response['error']) { + return [ + 'success' => false, + 'error' => true, + 'message' => $ai_response['message'] ?? 'Failed to generate options', + 'provider' => $this->current_provider + ]; + } + + return [ + 'success' => true, + 'options' => $ai_response['options'] ?? [], + 'provider' => $this->current_provider, + 'model' => $this->current_model + ]; + + } catch (\Exception $e) { + return [ + 'success' => false, + 'error' => true, + 'message' => $e->getMessage(), + 'provider' => $this->current_provider + ]; + } + } + + /** + * Get system prompt for field options generation + * + * @param string $field_type Field type + * @param string $output_format Output format + * @param string $tone Tone of options + * @param int $max_options Maximum number of options + * @return string System prompt + */ + private function get_field_options_system_prompt($field_type, $output_format, $tone, $max_options) { + $prompt = "You are an AI assistant helping to generate field options for a WordPress form.\n\n"; + $prompt .= "**Task:** Generate a list of options based on the user's request.\n\n"; + $prompt .= "**Field Type:** {$field_type}\n"; + $prompt .= "**Output Format:** {$output_format}\n"; + $prompt .= "**Tone:** {$tone}\n"; + $prompt .= "**Maximum Options:** {$max_options}\n\n"; + + $prompt .= "**Requirements:**\n"; + $prompt .= "1. Generate between 1 and {$max_options} options\n"; + $prompt .= "2. Each option should be clear, concise, and relevant\n"; + $prompt .= "3. Options should be appropriate for the specified tone\n"; + $prompt .= "4. Avoid duplicate or very similar options\n"; + $prompt .= "5. Use proper capitalization and formatting\n\n"; + + if ($output_format === 'value_label') { + $prompt .= "**Output Format:** Return a JSON object with 'options' array containing objects with 'value' and 'label' keys.\n"; + $prompt .= "Example:\n"; + $prompt .= "{\n"; + $prompt .= " \"options\": [\n"; + $prompt .= " {\"value\": \"option_1\", \"label\": \"Option 1\"},\n"; + $prompt .= " {\"value\": \"option_2\", \"label\": \"Option 2\"}\n"; + $prompt .= " ]\n"; + $prompt .= "}\n\n"; + } else { + $prompt .= "**Output Format:** Return a JSON object with 'options' array containing simple strings (one option per item).\n"; + $prompt .= "Example:\n"; + $prompt .= "{\n"; + $prompt .= " \"options\": [\"Option 1\", \"Option 2\", \"Option 3\"]\n"; + $prompt .= "}\n\n"; + } + + $prompt .= "**IMPORTANT:** Respond ONLY with valid JSON. Do not include any explanatory text, markdown formatting, or code blocks.\n"; + + return $prompt; + } + + /** + * Call OpenAI for field options generation + * + * @param string $system_prompt System prompt + * @param string $user_prompt User prompt + * @param array $options Additional options + * @return array Result + */ + private function call_openai_for_options($system_prompt, $user_prompt, $options = []) { + $model_config = $this->get_model_config('openai', $this->current_model); + + $body = [ + 'model' => $this->current_model, + 'messages' => [ + ['role' => 'system', 'content' => $system_prompt], + ['role' => 'user', 'content' => $user_prompt] + ], + 'temperature' => 0.7, + 'max_tokens' => 1000 + ]; + + if ($model_config['supports_json_mode']) { + $body['response_format'] = ['type' => 'json_object']; + } + + $args = [ + 'method' => 'POST', + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->api_key, + 'Content-Type' => 'application/json' + ], + 'body' => json_encode($body), + 'timeout' => 60 + ]; + + $response = wp_safe_remote_request($this->provider_configs['openai']['endpoint'], $args); + + if (is_wp_error($response)) { + throw new \Exception('OpenAI 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("OpenAI API returned HTTP {$status_code}: {$error_body}"); + } + + $body = wp_remote_retrieve_body($response); + $data = json_decode($body, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Exception('Invalid JSON response from OpenAI API'); + } + + 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'); + } + + return $this->parse_options_response($data['choices'][0]['message']['content'], $options); + } + + /** + * Call Anthropic for field options generation + * + * @param string $system_prompt System prompt + * @param string $user_prompt User prompt + * @param array $options Additional options + * @return array Result + */ + private function call_anthropic_for_options($system_prompt, $user_prompt, $options = []) { + $body = [ + 'model' => $this->current_model, + 'system' => $system_prompt, + 'messages' => [ + ['role' => 'user', 'content' => $user_prompt] + ], + 'temperature' => 0.7, + 'max_tokens' => 1000 + ]; + + $args = [ + 'method' => 'POST', + 'headers' => [ + 'x-api-key' => $this->api_key, + 'anthropic-version' => '2023-06-01', + 'Content-Type' => 'application/json' + ], + 'body' => json_encode($body), + 'timeout' => 60 + ]; + + $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); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Exception('Invalid JSON response from Anthropic API'); + } + + 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'); + } + + return $this->parse_options_response($data['content'][0]['text'], $options); + } + + /** + * Call Google for field options generation + * + * @param string $system_prompt System prompt + * @param string $user_prompt User prompt + * @param array $options Additional options + * @return array Result + */ + private function call_google_for_options($system_prompt, $user_prompt, $options = []) { + $model_config = $this->get_model_config('google', $this->current_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: " . $user_prompt] + ] + ] + ], + 'generationConfig' => [ + 'temperature' => 0.7, + 'maxOutputTokens' => 1000 + ] + ]; + + if ($model_config['supports_json_mode']) { + $body['generationConfig']['responseMimeType'] = 'application/json'; + } + + $args = [ + 'method' => 'POST', + 'headers' => [ + 'Content-Type' => 'application/json' + ], + 'body' => json_encode($body), + 'timeout' => 60 + ]; + + $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); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Exception('Invalid JSON response from Google API'); + } + + 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'); + } + + return $this->parse_options_response($data['candidates'][0]['content']['parts'][0]['text'], $options); + } + + /** + * Parse options from AI response + * + * @param string $content AI response content + * @param array $options Request options + * @return array Parsed options + */ + private function parse_options_response($content, $options = []) { + // 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 + $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 + $parsed = json_decode($json_content, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + return [ + 'error' => true, + 'message' => 'Unable to parse AI response. Please try again.' + ]; + } + + // Extract options from parsed response + $field_options = []; + $output_format = $options['output_format'] ?? 'one_per_line'; + + if (isset($parsed['options']) && is_array($parsed['options'])) { + if ($output_format === 'value_label') { + // Expecting array of objects with 'value' and 'label' + foreach ($parsed['options'] as $option) { + if (is_array($option) && isset($option['value']) && isset($option['label'])) { + $field_options[] = [ + 'label' => $option['label'], + 'value' => $option['value'] + ]; + } + } + } else { + // Expecting array of strings (one per line) + foreach ($parsed['options'] as $index => $option) { + if (is_string($option)) { + $field_options[] = [ + 'label' => $option, + 'value' => sanitize_title( $option ) + ]; + } + } + } + } + + return [ + 'error' => false, + 'options' => $field_options + ]; + } } diff --git a/includes/AI/RestController.php b/includes/AI/RestController.php index 1a1300f96..14ee5aa48 100644 --- a/includes/AI/RestController.php +++ b/includes/AI/RestController.php @@ -59,6 +59,7 @@ class RestController extends WP_REST_Controller { */ public function __construct() { $this->form_generator = new FormGenerator(); + add_action( 'wp_ajax_wpuf_ai_generate_field_options', [ $this, 'ajax_generate_field_options' ] ); } /** @@ -238,6 +239,48 @@ public function register_routes() { 'callback' => [$this, 'get_models'], 'permission_callback' => [$this, 'check_permission'] ]); + + // Generate field options endpoint + register_rest_route($this->namespace, '/' . $this->rest_base . '/generate-options', [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [$this, 'generate_field_options'], + 'permission_callback' => [$this, 'check_permission'], + 'args' => [ + 'prompt' => [ + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_textarea_field', + 'validate_callback' => [$this, 'validate_prompt'] + ], + 'field_type' => [ + 'required' => true, + 'type' => 'string', + 'enum' => ['dropdown_field', 'radio_field', 'checkbox_field', 'multiple_select'], + 'sanitize_callback' => 'sanitize_text_field' + ], + 'output_format' => [ + 'required' => false, + 'type' => 'string', + 'enum' => ['one_per_line', 'value_label'], + 'default' => 'one_per_line', + 'sanitize_callback' => 'sanitize_text_field' + ], + 'tone' => [ + 'required' => false, + 'type' => 'string', + 'enum' => ['casual', 'formal', 'professional', 'friendly'], + 'default' => 'casual', + 'sanitize_callback' => 'sanitize_text_field' + ], + 'max_options' => [ + 'required' => false, + 'type' => 'integer', + 'minimum' => 1, + 'maximum' => 100, + 'default' => 20 + ] + ] + ]); } /** @@ -583,6 +626,123 @@ public function get_models(WP_REST_Request $request) { ], 200); } + /** + * Generate field options using AI + * + * @param WP_REST_Request $request REST request object + * @return WP_REST_Response|WP_Error Response object + */ + public function generate_field_options(WP_REST_Request $request) { + try { + $prompt = $request->get_param('prompt'); + $field_type = $request->get_param('field_type'); + $output_format = $request->get_param('output_format') ?? 'one_per_line'; + $tone = $request->get_param('tone') ?? 'casual'; + $max_options = $request->get_param('max_options') ?? 20; + + // Validate max options + if ($max_options < 1 || $max_options > 100) { + return new WP_Error( + 'invalid_max_options', + __('Maximum options must be between 1 and 100', 'wp-user-frontend'), + ['status' => 400] + ); + } + + // Call FormGenerator to generate options + $result = $this->form_generator->generate_field_options($prompt, [ + 'field_type' => $field_type, + 'output_format' => $output_format, + 'tone' => $tone, + 'max_options' => $max_options + ]); + + if (isset($result['error']) && $result['error']) { + return new WP_Error( + 'generation_failed', + $result['message'] ?? __('Failed to generate field options', 'wp-user-frontend'), + ['status' => 400] + ); + } + + // Sanitize generated options + $sanitized_options = $this->sanitize_field_options($result['options'] ?? []); + + return new WP_REST_Response([ + 'success' => true, + 'options' => $sanitized_options, + 'message' => __('Options generated successfully', 'wp-user-frontend') + ], 200); + + } catch (\Exception $e) { + return new WP_Error( + 'generation_error', + __('An error occurred while generating options. Please try again.', 'wp-user-frontend'), + ['status' => 500] + ); + } + } + + /** + * Sanitize field options + * + * @param array $options Raw options from AI + * @return array Sanitized options + */ + private function sanitize_field_options($options) { + if (!is_array($options)) { + return []; + } + + $sanitized = []; + foreach ($options as $key => $value) { + // Generate safe key from value if numeric key + if (is_numeric($key)) { + $safe_key = sanitize_key(strtolower(str_replace(' ', '_', $value))); + } else { + $safe_key = sanitize_key($key); + } + + $sanitized[$safe_key] = sanitize_text_field($value); + } + + return $sanitized; + } + + /** + * AJAX handler for generating field options + * + * @since 4.2.2 + * + * @return void + */ + public function ajax_generate_field_options() { + check_ajax_referer( 'form-builder-setting-nonce', 'nonce' ); + + if ( ! $this->check_permission() ) { + wp_send_json_error( [ 'message' => __( 'Permission denied', 'wp-user-frontend' ) ] ); + } + + $prompt = sanitize_textarea_field( $_POST['prompt'] ?? '' ); + $field_type = sanitize_text_field( $_POST['field_type'] ?? 'dropdown_field' ); + + if ( empty( $prompt ) ) { + wp_send_json_error( [ 'message' => __( 'Prompt is required', 'wp-user-frontend' ) ] ); + } + + $result = $this->form_generator->generate_field_options( $prompt, [ 'field_type' => $field_type ] ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( [ 'message' => $result->get_error_message() ] ); + } + + if ( isset( $result['success'] ) && $result['success'] ) { + wp_send_json_success( [ 'options' => $result['options'] ] ); + } + + wp_send_json_error( [ 'message' => $result['message'] ?? __( 'Failed to generate options', 'wp-user-frontend' ) ] ); + } + /** * Check if user has permission to use AI form builder * diff --git a/includes/Admin/Forms/Admin_Form_Builder.php b/includes/Admin/Forms/Admin_Form_Builder.php index fdb827e16..87aee3c05 100644 --- a/includes/Admin/Forms/Admin_Form_Builder.php +++ b/includes/Admin/Forms/Admin_Form_Builder.php @@ -178,6 +178,15 @@ public function admin_enqueue_scripts() { // Load icon configuration $icon_config = $this->get_icon_config(); + // Check AI configuration directly + $ai_settings = get_option( 'wpuf_ai', [] ); + $ai_provider = isset( $ai_settings['ai_provider'] ) ? $ai_settings['ai_provider'] : ''; + $ai_model = isset( $ai_settings['ai_model'] ) ? $ai_settings['ai_model'] : ''; + $provider_key = $ai_provider . '_api_key'; + $ai_api_key = isset( $ai_settings[ $provider_key ] ) ? $ai_settings[ $provider_key ] : ''; + $ai_configured = ! empty( $ai_provider ) && ! empty( $ai_api_key ) && ! empty( $ai_model ); + $ai_settings_url = admin_url( 'admin.php?page=wpuf-settings#/ai' ); + $wpuf_form_builder = apply_filters( 'wpuf_form_builder_localize_script', [ @@ -208,6 +217,8 @@ public function admin_enqueue_scripts() { 'free_icon' => $free_icon, 'icons' => $icon_config['icons'], 'defaultIcons' => $icon_config['defaultIcons'], + 'ai_configured' => $ai_configured, + 'ai_settings_url' => $ai_settings_url, ] ); $wpuf_form_builder = wpuf_unset_conditional( $wpuf_form_builder ); diff --git a/src/css/admin/form-builder.css b/src/css/admin/form-builder.css index 22d77e729..2c1dec38a 100644 --- a/src/css/admin/form-builder.css +++ b/src/css/admin/form-builder.css @@ -317,3 +317,39 @@ button.swal2-cancel.swal2-styled.swal2-default-outline { .wpuf-pro-modal-right img { @apply wpuf-w-auto wpuf-h-80 wpuf-rounded wpuf-block; } + +/* AI Field Options Modal */ +.wpuf-ai-modal-overlay { + @apply wpuf-fixed wpuf-inset-0 wpuf-bg-black wpuf-bg-opacity-50 wpuf-flex wpuf-items-center wpuf-justify-center wpuf-z-50; +} + +.wpuf-ai-modal { + @apply wpuf-bg-white wpuf-rounded-lg wpuf-shadow-xl wpuf-max-w-2xl wpuf-w-full wpuf-mx-4; + max-height: 90vh; +} + +.wpuf-ai-modal-header { + @apply wpuf-flex wpuf-justify-between wpuf-items-center wpuf-p-6 wpuf-border-b; +} + +.wpuf-ai-modal-header h3 { + @apply wpuf-text-lg wpuf-font-semibold wpuf-m-0; +} + +.wpuf-ai-modal-close { + @apply wpuf-text-gray-400 hover:wpuf-text-gray-600 wpuf-text-3xl wpuf-leading-none wpuf-bg-transparent wpuf-border-0 wpuf-cursor-pointer; +} + +.wpuf-ai-modal-body { + @apply wpuf-p-6; + max-height: 60vh; + overflow-y: auto; +} + +.wpuf-ai-modal-footer { + @apply wpuf-flex wpuf-justify-end wpuf-gap-3 wpuf-p-6 wpuf-border-t; +} + +.wpuf-ai-options-list { + @apply wpuf-max-h-64 wpuf-overflow-y-auto wpuf-border wpuf-rounded wpuf-p-3; +} From 7d74ff27ee3b80db2136985435dcdc03696ae6f6 Mon Sep 17 00:00:00 2001 From: Ariful Hoque Date: Tue, 2 Dec 2025 13:24:13 +0600 Subject: [PATCH 2/2] Update build:css script to run tailwind before minify The build:css npm script now runs 'grunt tailwind' before 'grunt tailwind-minify' to ensure Tailwind CSS is processed prior to minification. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc5222a19..22cca4c4a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "build:subscriptions": "ENTRY=subscriptions vite build", "build:frontend-subscriptions": "ENTRY=frontend-subscriptions vite build", "build:ai-form-builder": "ENTRY=ai-form-builder vite build", - "build:css": "grunt tailwind-minify" + "build:css": "grunt tailwind && grunt tailwind-minify" }, "devDependencies": { "@tailwindcss/forms": "^0.5.7",