generateForm($this->app->request->all()); $form = $this->prepareAndSaveForm($form); wp_send_json_success([ 'formId' => $form->id, 'redirect_url' => admin_url( 'admin.php?page=fluent_forms&form_id=' . $form->id . '&route=editor' ), 'message' => __('Successfully created a form.', 'fluentform'), ], 200); } catch (Exception $e) { wp_send_json_error([ 'message' => $e->getMessage(), ], 422); } } /** * @param array $form * @return Form|\FluentForm\Framework\Database\Query\Builder * @throws Exception */ protected function prepareAndSaveForm($form) { $allFields = $this->getDefaultFields(); $fluentFormFields = []; $fields = Arr::get($form, 'fields', []); $isConversational = Arr::isTrue($form, 'is_conversational'); $customCss = Arr::get($form, 'custom_css', ''); $hasStep = false; $lastFieldIndex = count($fields) - 1; $disableFields = array_keys($this->getDisabledComponents()); foreach ($fields as $index => $field) { if (count($field) == 1) { $field = reset($field); } if ($element = $this->resolveInput($field)) { if (in_array($element, $disableFields)) { continue; } if (!$hasStep && 'form_step' === $element) { if (0 === $index || $lastFieldIndex === $index) { continue; } $hasStep = true; } $fluentFormFields[] = $this->processField($element, $field, $allFields); } } $fluentFormFields = array_filter($fluentFormFields); if (!$fluentFormFields) { throw new Exception(esc_html__('Empty form. Please try again!', 'fluentform')); } $title = Arr::get($form, 'title', ''); return $this->saveForm($fluentFormFields, $title, $hasStep, $isConversational, $customCss); } /** * @param array $args * @return array response form fields * @throws Exception */ protected function generateForm($args) { $aiModel = Arr::get($args, 'ai_model', 'default'); $isUsingChatGpt = Helper::hasPro() && 'chat_gpt' == $aiModel && class_exists('FluentFormPro\classes\Chat\ChatFormBuilder'); if ($isUsingChatGpt) { (new \FluentFormPro\classes\Chat\ChatFormBuilder())->buildForm(); } $paymentSetting = PaymentHelper::getPaymentSettings(); $queryArgs = [ 'user_prompt' => $this->getUserPrompt($args), 'site_url' => site_url(), 'site_title' => get_bloginfo('name'), 'site_locale' => determine_locale(), 'has_pro' => Helper::hasPro(), 'has_payment' => $paymentSetting['status'] == 'yes', 'request_id' => uniqid('ff_ai_') ]; $result = (new FluentFormAIAPI())->makeRequest($queryArgs); if (is_wp_error($result)) { throw new Exception(esc_html($result->get_error_message())); } $response = trim(Arr::get($result, 'response', ''), '"'); if (false !== preg_match('/```json(.*?)```/s', $response, $matches)) { $response = trim($matches[1]); } $decoded = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE || empty($decoded) || empty($decoded['fields'])) { throw new Exception(esc_html__('Invalid response: Please try again!', 'fluentform')); } return $this->applyPromptHints($decoded, $args); } protected function getDefaultFields() { if ($this->allDefaultFields) { return $this->allDefaultFields; } /** * @var \FluentForm\App\Services\FormBuilder\Components */ $components = $this->app->make('components'); $this->app->doAction('fluentform/editor_init', $components); $editorComponents = $components->toArray(); $general = Arr::get($editorComponents, 'general', []); $advanced = Arr::get($editorComponents, 'advanced', []); $container = Arr::get($editorComponents, 'container', []); // Apply filter to get additional components // The second parameter (true) is passed to prevent field loss when form ID is falsy loss some field $editorComponents = apply_filters('fluentform/editor_components', [], true); if ($generalExtra = Arr::get($editorComponents, 'general')) { $generalExtra = array_column($generalExtra, null, 'element'); $general = array_merge($general, $generalExtra); } if ($advancedExtra = Arr::get($editorComponents, 'advanced')) { $advancedExtra = array_column($advancedExtra, null, 'element'); $advanced = array_merge($advanced, $advancedExtra); } $payments = Arr::get($editorComponents, 'payments', []); $payments = array_column($payments, null, 'element'); $this->allDefaultFields = array_merge($general, $payments, $advanced, ['container' => $container]); return $this->allDefaultFields; } protected function processField($element, $field, $allFields) { if ('container' == $element) { return $this->resolveContainerFields($field, $allFields); } $matchedField = Arr::get($allFields, $element); if (!$matchedField) { return []; } $formatField = $matchedField; if ($settings = Arr::get($field, 'settings')) { // Replace 'label' with 'admin_field_label' if 'label' is shorter if (isset($settings['label']) && $adminFieldLabel = Arr::get($settings, 'admin_field_label')) { if (strlen($settings['label']) < strlen($adminFieldLabel)) { $settings['label'] = $adminFieldLabel; } } $formatField['settings'] = wp_parse_args($settings, $matchedField['settings']); } if ($attributes = Arr::get($field, 'attributes')) { $formatField['attributes'] = wp_parse_args($attributes, $matchedField['attributes']); } $formatField['uniqElKey'] = "el_" . uniqid(); if ('form_step' === $element) { return $formatField; } if ($fieldName = Arr::get($field, 'attributes.name')) { $formatField['attributes']['name'] = $fieldName; } if ($options = $this->getOptions(Arr::get($field, 'options'))) { if (isset($formatField['settings']['advanced_options'])) { $formatField['settings']['advanced_options'] = $options; } if ('ratings' == $element) { $formatField['options'] = array_column($options, 'label', 'value'); } } if ('rangeslider' == $element) { if ($min = Arr::get($field, 'min')) { $formatField['attributes']['min'] = intval($min); } if ($max = intval(Arr::get($field, 'max', 10))) { $formatField['attributes']['max'] = $max; } } if (in_array($element, ['input_name', 'address']) && $fields = Arr::get($field, 'fields')) { foreach ($formatField['fields'] as $name => &$field) { if ($targetAttributes = Arr::get($fields, "$name.attributes")) { $field['attributes'] = wp_parse_args($targetAttributes, $field['attributes']); } if ($targetSettings = Arr::get($fields, "$name.settings")) { $field['settings'] = wp_parse_args($targetSettings, $field['settings']); } } } return $formatField; } protected function resolveInput($field) { if (!is_array($field)) { return false; } $element = Arr::get($field, 'element'); $allElements = array_keys($this->getDefaultFields()); if (in_array($element, $allElements)) { return $element; } $type = Arr::get($field, 'type'); if (!$type) { return false; } $searchTags = fluentformLoadFile('Services/FormBuilder/ElementSearchTags.php'); $form = ['type' => '']; $form = json_decode(json_encode($form)); $searchTags = apply_filters('fluentform/editor_element_search_tags', $searchTags, $form); foreach ($searchTags as $inputKey => $tags) { if (array_search($type, $tags) !== false) { return $inputKey; } else { foreach ($tags as $tag) { if (strpos($tag, $type) !== false) { return $inputKey; } } } } return false; } protected function getOptions($options = []) { $formattedOptions = []; if (empty($options) || !is_array($options)) { return $options; } foreach ($options as $key => $option) { if (is_string($option) || is_numeric($option)) { $value = $label = $option; } elseif (is_array($option)) { $label = Arr::get($option, 'label'); $value = Arr::get($option, 'value'); } else { continue; } if (!$value || !$label) { $value = $value ?? $label; $label = $label ?? $value; } if (!$value || !$label) { continue; } $formattedOptions[] = [ 'label' => $label, 'value' => $value, ]; } return $formattedOptions; } protected function getBlankFormConfig() { $attributes = ['type' => 'form', 'predefined' => 'blank_form']; $customForm = Form::resolvePredefinedForm($attributes); $customForm['form_fields'] = json_decode($customForm['form_fields'], true); $customForm['form_fields']['submitButton'] = $customForm['form']['submitButton']; $customForm['form_fields'] = json_encode($customForm['form_fields']); return $customForm; } protected function saveForm($formattedInputs, $title, $isStepForm = false, $isConversational = false, $customCss = '') { $customForm = $this->prepareCustomForm($formattedInputs, $isStepForm); $data = Form::prepare($customForm); $form = $this->model->create($data); $form->title = $title ?: $form->title . ' (ChatGPT#' . $form->id . ')'; $formData = (object)$form->toArray(); if (FormFieldsParser::hasPaymentFields($formData)) { $form->has_payment = 1; } if ($isConversational) { $formMeta = FormMeta::prepare(['type' => 'form', 'predefined' => 'conversational'], $customForm); $form->fill([ 'form_fields' => Converter::convertExistingForm($form), ])->save(); } else { $form->save(); $formMeta = FormMeta::prepare(['type' => 'form', 'predefined' => 'blank_form'], $customForm); } FormMeta::store($form, $formMeta); if ($customCss = fluentformSanitizeCSS($customCss)) { Helper::setFormMeta($form->id, '_custom_form_css', $customCss); } do_action('fluentform/inserted_new_form', $form->id, $data); return $form; } protected function prepareCustomForm($formattedInputs, $isStepForm) { $formattedInputs = fluentFormSanitizer($formattedInputs); $customForm = $this->getBlankFormConfig(); $fields = json_decode($customForm['form_fields'], true); $fields['form_fields']['fields'] = $formattedInputs; $fields['form_fields']['submitButton'] = Arr::get($customForm, 'form.submitButton'); if ($isStepForm) { $fields['form_fields']['stepsWrapper'] = $this->getStepWrapper(); } $customForm['form_fields'] = json_encode($fields['form_fields']); return $customForm; } protected function resolveContainerFields($field, $allFields) { $columns = Arr::get($field, 'columns'); $columnsCount = count($columns); if (!$columnsCount || $columnsCount > 6) { return []; } $matchedField = Arr::get($allFields, 'container.container_' . $columnsCount . '_col'); if (!$matchedField) { return []; } $columnWidth = round(100 / $columnsCount, 2); foreach ($columns as &$column) { $formatedFields = []; $fields = Arr::get($column, 'fields', []); $columnWidth = Arr::get($column, 'width', $columnWidth); foreach ($fields as $colField) { $element = Arr::get($colField, 'element'); if ($columnField = $this->processField($element, $colField, $allFields)) { $formatedFields[] = $columnField; } } if ($formatedFields) { $column['fields'] = $formatedFields; $column['width'] = $columnWidth; } } $matchedField['columns'] = $columns; return $matchedField; } /** * @return array */ protected function getStepWrapper() { return [ 'stepStart' => [ 'element' => 'step_start', 'attributes' => [ 'id' => '', 'class' => '', ], 'settings' => [ 'progress_indicator' => 'progress-bar', 'step_titles' => [], 'disable_auto_focus' => 'no', 'enable_auto_slider' => 'no', 'enable_step_data_persistency' => 'no', 'enable_step_page_resume' => 'no', ], 'editor_options' => [ 'title' => 'Start Paging' ], ], 'stepEnd' => [ 'element' => 'step_end', 'attributes' => [ 'id' => '', 'class' => '', ], 'settings' => [ 'prev_btn' => [ 'type' => 'default', 'text' => 'Previous', 'img_url' => '' ] ], 'editor_options' => [ 'title' => 'End Paging' ], ] ]; } private function getUserPrompt($args) { $startingQuery = "Create a form for "; $query = Sanitizer::sanitizeTextField(Arr::get($args, 'query')); if (empty($query)) { throw new Exception(esc_html__('Query is empty!', 'fluentform')); } // Validate query length to prevent abuse (max 2000 characters) if (strlen($query) > 2000) { throw new Exception(esc_html__('Query is too long. Please limit your prompt to 2000 characters.', 'fluentform')); } $additionalQuery = Sanitizer::sanitizeTextField(Arr::get($args, 'additional_query')); // Validate additional query length (max 1000 characters) if ($additionalQuery && strlen($additionalQuery) > 1000) { throw new Exception(esc_html__('Additional query is too long. Please limit to 1000 characters.', 'fluentform')); } if ($additionalQuery) { $query .= "\n including questions for information like " . $additionalQuery . "."; } return $startingQuery . $query . $this->getPromptContractInstructions(); } private function getPromptContractInstructions() { return "\n\nReturn strict JSON only. The user's instructions may be written in any language. " . "Preserve labels and help text in the user's original language, but always return machine-readable field settings. " . "For every field, explicitly set whether it is required in settings.validation_rules.required.value when the prompt marks it as required or optional. " . "If the prompt lists allowed upload extensions, map them into settings.validation_rules.allowed_file_types.value. " . "If the prompt asks for a multi-step form with sections, include form_step elements between sections."; } private function applyPromptHints(array $form, array $args) { $query = (string) Arr::get($args, 'query', ''); $additionalQuery = (string) Arr::get($args, 'additional_query', ''); $hints = $this->extractPromptFieldHints(trim($query . "\n" . $additionalQuery)); if (!$hints) { return $form; } $hintIndex = 0; $form['fields'] = $this->applyHintsToFields(Arr::get($form, 'fields', []), $hints, $hintIndex); return $form; } private function applyHintsToFields(array $fields, array $hints, &$hintIndex) { foreach ($fields as &$field) { if (!is_array($field)) { continue; } if (count($field) === 1) { $field = reset($field); } $element = Arr::get($field, 'element'); if ('container' === $element) { $columns = Arr::get($field, 'columns', []); foreach ($columns as &$column) { $column['fields'] = $this->applyHintsToFields(Arr::get($column, 'fields', []), $hints, $hintIndex); } $field['columns'] = $columns; continue; } if (in_array($element, ['form_step', 'section_break'])) { continue; } $hint = Arr::get($hints, $hintIndex); $hintIndex++; if (!$hint) { continue; } $field = $this->mergeFieldHint($field, $hint); } return $fields; } private function mergeFieldHint(array $field, array $hint) { if (array_key_exists('required', $hint)) { Arr::set($field, 'settings.validation_rules.required.value', (bool) $hint['required']); } if (!empty($hint['help_message'])) { Arr::set($field, 'settings.help_message', $hint['help_message']); } if (!empty($hint['allowed_file_types']) && in_array(Arr::get($field, 'element'), ['input_file', 'input_image'])) { Arr::set($field, 'settings.validation_rules.allowed_file_types.value', array_values(array_unique($hint['allowed_file_types']))); } return $field; } private function extractPromptFieldHints($prompt) { if (!$prompt) { return []; } $lines = preg_split('/\R/u', $prompt); $hints = []; foreach ($lines as $line) { $line = trim(wp_strip_all_tags($line)); $line = preg_replace('/^[\-\*\d\.\)\s]+/u', '', $line); if (!$line || !$this->looksLikeFieldLine($line)) { continue; } $attributesText = ''; if (preg_match('/\(([^()]*)\)/u', $line, $matches)) { $attributesText = trim($matches[1]); } $hint = []; $normalizedLine = function_exists('mb_strtolower') ? mb_strtolower($line, 'UTF-8') : strtolower($line); if ($this->containsAnyPhrase($normalizedLine, $this->getRequiredKeywords())) { $hint['required'] = true; } elseif ($this->containsAnyPhrase($normalizedLine, $this->getOptionalKeywords())) { $hint['required'] = false; } $helpMessage = $this->extractHelpMessage($attributesText); if ($helpMessage) { $hint['help_message'] = $helpMessage; } if ($this->containsAnyPhrase($normalizedLine, $this->getFileUploadKeywords())) { $allowedTypes = $this->extractAllowedFileTypes($line); if ($allowedTypes) { $hint['allowed_file_types'] = $allowedTypes; } } $hints[] = $hint; } return $hints; } private function looksLikeFieldLine($line) { if (preg_match('/^(create|erstelle|crée|crear|criar|crea|maak|utw[oó]rz|oluştur|创建|作成)\b/ui', $line)) { return false; } if (preg_match('/^(section|abschnitt|secci[oó]n|sectione|seção|sectie|sekcja|b[oö]l[uü]m|章节|セクション)\b/ui', $line)) { return false; } if (false === strpos($line, '(')) { return false; } preg_match('/\(([^()]*)\)/u', $line, $matches); $attributesText = isset($matches[1]) ? $matches[1] : ''; return $this->containsAnyPhrase( function_exists('mb_strtolower') ? mb_strtolower($attributesText, 'UTF-8') : strtolower($attributesText), $this->getFieldDescriptorKeywords() ); } private function extractHelpMessage($attributesText) { if (!$attributesText) { return ''; } if (preg_match('/(?:note|hint|hinweis|remarque|nota|nota bene|opmerking|uwaga|not|说明|備考)\s*:\s*["“]?(.+?)["”]?$/ui', $attributesText, $matches)) { return trim($matches[1], " \t\n\r\0\x0B\"'“”"); } return ''; } private function extractAllowedFileTypes($line) { preg_match_all('/\b(jpe?g|gif|png|tiff?|bmp|webp|svg|pdf|docx?|xlsx?|xls|csv|zip|rar|txt)\b/ui', $line, $matches); if (empty($matches[1])) { return []; } $types = array_map(function ($type) { $type = strtolower($type); if ('jpeg' === $type) { return 'jpg'; } if ('tif' === $type) { return 'tiff'; } return $type; }, $matches[1]); return array_values(array_unique($types)); } private function containsAnyPhrase($text, array $phrases) { foreach ($phrases as $phrase) { if (false !== strpos($text, $phrase)) { return true; } } return false; } private function getRequiredKeywords() { return [ 'required', 'mandatory', 'pflichtfeld', 'erforderlich', 'obligatoire', 'requis', 'obligatorio', 'obrigatório', 'obbligatorio', 'verplicht', 'wymagane', 'zorunlu', '必填', '必須' ]; } private function getOptionalKeywords() { return [ 'optional', 'facultatif', 'facoltativo', 'opcjonalne', 'isteğe bağlı', 'opcional', 'opcionales', '选填', '任意' ]; } private function getFileUploadKeywords() { return [ 'file upload', 'upload', 'datei', 'datei upload', 'foto upload', 'image upload', 'upload de fichier', 'carga de archivo', 'subida de archivo', 'carregamento de arquivo', 'caricamento file', 'bestandsupload', 'yükleme', '上传' ]; } private function getFieldDescriptorKeywords() { return [ 'text', 'textarea', 'email', 'date', 'file', 'upload', 'phone', 'number', 'radio', 'checkbox', 'select', 'dropdown', 'image', 'foto', 'bild', 'datei', 'telefon', 'mobil', 'correo', 'texte', 'texto', 'testo', 'fecha', 'fichier', 'archivo', 'caricamento', 'ficheiro', 'bestand', 'yükleme', '上传' ]; } }