PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.2
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.2
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Modules / Ai / AiFormBuilder.php

AiFormBuilder.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.2, at app/Modules/Ai/AiFormBuilder.php

460 lines 16.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Modules\Ai;
4
5 defined('ABSPATH') or die;
6
7 use Exception;
8 use FluentForm\App\Helpers\Helper;
9 use FluentForm\App\Models\Form;
10 use FluentForm\App\Models\FormMeta;
11 use FluentForm\App\Modules\Acl\Acl;
12 use FluentForm\App\Modules\Form\FormFieldsParser;
13 use FluentForm\App\Modules\Payments\PaymentHelper;
14 use FluentForm\App\Services\FluentConversational\Classes\Converter\Converter;
15 use FluentForm\App\Services\Form\FormService;
16 use FluentForm\Framework\Helpers\ArrayHelper as Arr;
17 use FluentForm\Framework\Support\Sanitizer;
18
19 class AiFormBuilder extends FormService
20 {
21 private $allDefaultFields = [];
22
23 public function __construct()
24 {
25 parent::__construct();
26 add_action('wp_ajax_fluentform_ai_create_form', [$this, 'buildForm'], 11, 0);
27 }
28
29 public function buildForm()
30 {
31 try {
32 Acl::verify('fluentform_forms_manager');
33 $form = $this->generateForm($this->app->request->all());
34 $form = $this->prepareAndSaveForm($form);
35 wp_send_json_success([
36 'formId' => $form->id,
37 'redirect_url' => admin_url(
38 'admin.php?page=fluent_forms&form_id=' . $form->id . '&route=editor'
39 ),
40 'message' => __('Successfully created a form.', 'fluentform'),
41 ], 200);
42 } catch (Exception $e) {
43 wp_send_json_error([
44 'message' => $e->getMessage(),
45 ], 422);
46 }
47 }
48
49 /**
50 * @param array $form
51 * @return Form|\FluentForm\Framework\Database\Query\Builder
52 * @throws Exception
53 */
54 protected function prepareAndSaveForm($form)
55 {
56 $allFields = $this->getDefaultFields();
57 $fluentFormFields = [];
58 $fields = Arr::get($form, 'fields', []);
59 $isConversational = Arr::isTrue($form, 'is_conversational');
60 $customCss = Arr::get($form, 'custom_css', '');
61 $hasStep = false;
62 $lastFieldIndex = count($fields) - 1;
63
64 $disableFields = array_keys($this->getDisabledComponents());
65 foreach ($fields as $index => $field) {
66 if (count($field) == 1) {
67 $field = reset($field);
68 }
69 if ($element = $this->resolveInput($field)) {
70 if (in_array($element, $disableFields)) {
71 continue;
72 }
73 if (!$hasStep && 'form_step' === $element) {
74 if (0 === $index || $lastFieldIndex === $index) {
75 continue;
76 }
77 $hasStep = true;
78 }
79 $fluentFormFields[] = $this->processField($element, $field, $allFields);
80 }
81 }
82 $fluentFormFields = array_filter($fluentFormFields);
83 if (!$fluentFormFields) {
84 throw new Exception(esc_html__('Empty form. Please try again!', 'fluentform'));
85 }
86 $title = Arr::get($form, 'title', '');
87 return $this->saveForm($fluentFormFields, $title, $hasStep, $isConversational, $customCss);
88 }
89
90 /**
91 * @param array $args
92 * @return array response form fields
93 * @throws Exception
94 */
95 protected function generateForm($args)
96 {
97 $aiModel = Arr::get($args, 'ai_model', 'default');
98 $isUsingChatGpt = Helper::hasPro() && 'chat_gpt' == $aiModel && class_exists('FluentFormPro\classes\Chat\ChatFormBuilder');
99
100 if ($isUsingChatGpt) {
101 (new \FluentFormPro\classes\Chat\ChatFormBuilder())->buildForm();
102 }
103
104 $paymentSetting = PaymentHelper::getPaymentSettings();
105 $queryArgs = [
106 'user_prompt' => $this->getUserPrompt($args),
107 'site_url' => site_url(),
108 'site_title' => get_bloginfo('name'),
109 'has_pro' => Helper::hasPro(),
110 'has_payment' => $paymentSetting['status'] == 'yes',
111 'request_id' => uniqid('ff_ai_')
112 ];
113
114 $result = (new FluentFormAIAPI())->makeRequest($queryArgs);
115
116 if (is_wp_error($result)) {
117 throw new Exception(esc_html($result->get_error_message()));
118 }
119
120 $response = trim(Arr::get($result, 'response', ''), '"');
121 if (false !== preg_match('/```json(.*?)```/s', $response, $matches)) {
122 $response = trim($matches[1]);
123 }
124
125 $decoded = json_decode($response, true);
126 if (json_last_error() !== JSON_ERROR_NONE || empty($decoded) || empty($decoded['fields'])) {
127 throw new Exception(esc_html__('Invalid response: Please try again!', 'fluentform'));
128 }
129 return $decoded;
130 }
131
132 protected function getDefaultFields()
133 {
134 if ($this->allDefaultFields) {
135 return $this->allDefaultFields;
136 }
137 /**
138 * @var \FluentForm\App\Services\FormBuilder\Components
139 */
140 $components = $this->app->make('components');
141 $this->app->doAction('fluentform/editor_init', $components);
142 $editorComponents = $components->toArray();
143 $general = Arr::get($editorComponents, 'general', []);
144 $advanced = Arr::get($editorComponents, 'advanced', []);
145 $container = Arr::get($editorComponents, 'container', []);
146
147 // Apply filter to get additional components
148 // The second parameter (true) is passed to prevent field loss when form ID is falsy loss some field
149 $editorComponents = apply_filters('fluentform/editor_components', [], true);
150 if ($generalExtra = Arr::get($editorComponents, 'general')) {
151 $generalExtra = array_column($generalExtra, null, 'element');
152 $general = array_merge($general, $generalExtra);
153 }
154
155 if ($advancedExtra = Arr::get($editorComponents, 'advanced')) {
156 $advancedExtra = array_column($advancedExtra, null, 'element');
157 $advanced = array_merge($advanced, $advancedExtra);
158 }
159
160 $payments = Arr::get($editorComponents, 'payments', []);
161 $payments = array_column($payments, null, 'element');
162 $this->allDefaultFields = array_merge($general, $payments, $advanced, ['container' => $container]);
163 return $this->allDefaultFields;
164 }
165
166 protected function processField($element, $field, $allFields)
167 {
168 if ('container' == $element) {
169 return $this->resolveContainerFields($field, $allFields);
170 }
171
172 $matchedField = Arr::get($allFields, $element);
173 if (!$matchedField) {
174 return [];
175 }
176 $formatField = $matchedField;
177 if ($settings = Arr::get($field, 'settings')) {
178 // Replace 'label' with 'admin_field_label' if 'label' is shorter
179 if (isset($settings['label']) && $adminFieldLabel = Arr::get($settings, 'admin_field_label')) {
180 if (strlen($settings['label']) < strlen($adminFieldLabel)) {
181 $settings['label'] = $adminFieldLabel;
182 }
183 }
184 $formatField['settings'] = wp_parse_args($settings, $matchedField['settings']);
185 }
186 if ($attributes = Arr::get($field, 'attributes')) {
187 $formatField['attributes'] = wp_parse_args($attributes, $matchedField['attributes']);
188 }
189
190 $formatField['uniqElKey'] = "el_" . uniqid();
191
192 if ('form_step' === $element) {
193 return $formatField;
194 }
195
196 if ($fieldName = Arr::get($field, 'attributes.name')) {
197 $formatField['attributes']['name'] = $fieldName;
198 }
199
200 if ($options = $this->getOptions(Arr::get($field, 'options'))) {
201 if (isset($formatField['settings']['advanced_options'])) {
202 $formatField['settings']['advanced_options'] = $options;
203 }
204 if ('ratings' == $element) {
205 $formatField['options'] = array_column($options, 'label', 'value');
206 }
207 }
208
209 if ('rangeslider' == $element) {
210 if ($min = Arr::get($field, 'min')) {
211 $formatField['attributes']['min'] = intval($min);
212 }
213 if ($max = intval(Arr::get($field, 'max', 10))) {
214 $formatField['attributes']['max'] = $max;
215 }
216 }
217
218 if (in_array($element, ['input_name', 'address']) && $fields = Arr::get($field, 'fields')) {
219 foreach ($formatField['fields'] as $name => &$field) {
220 if ($targetAttributes = Arr::get($fields, "$name.attributes")) {
221 $field['attributes'] = wp_parse_args($targetAttributes, $field['attributes']);
222 }
223 if ($targetSettings = Arr::get($fields, "$name.settings")) {
224 $field['settings'] = wp_parse_args($targetSettings, $field['settings']);
225 }
226 }
227 }
228
229 return $formatField;
230 }
231
232 protected function resolveInput($field)
233 {
234 if (!is_array($field)) {
235 return false;
236 }
237 $element = Arr::get($field, 'element');
238 $allElements = array_keys($this->getDefaultFields());
239 if (in_array($element, $allElements)) {
240 return $element;
241 }
242
243 $type = Arr::get($field, 'type');
244 if (!$type) {
245 return false;
246 }
247
248 $searchTags = fluentformLoadFile('Services/FormBuilder/ElementSearchTags.php');
249 $form = ['type' => ''];
250 $form = json_decode(json_encode($form));
251 $searchTags = apply_filters('fluentform/editor_element_search_tags', $searchTags, $form);
252 foreach ($searchTags as $inputKey => $tags) {
253 if (array_search($type, $tags) !== false) {
254 return $inputKey;
255 } else {
256 foreach ($tags as $tag) {
257 if (strpos($tag, $type) !== false) {
258 return $inputKey;
259 }
260 }
261 }
262 }
263 return false;
264 }
265
266 protected function getOptions($options = [])
267 {
268 $formattedOptions = [];
269 if (empty($options) || !is_array($options)) {
270 return $options;
271 }
272 foreach ($options as $key => $option) {
273 if (is_string($option) || is_numeric($option)) {
274 $value = $label = $option;
275 } elseif (is_array($option)) {
276 $label = Arr::get($option, 'label');
277 $value = Arr::get($option, 'value');
278 } else {
279 continue;
280 }
281 if (!$value || !$label) {
282 $value = $value ?? $label;
283 $label = $label ?? $value;
284 }
285 if (!$value || !$label) {
286 continue;
287 }
288 $formattedOptions[] = [
289 'label' => $label,
290 'value' => $value,
291 ];
292 }
293
294 return $formattedOptions;
295 }
296
297 protected function getBlankFormConfig()
298 {
299 $attributes = ['type' => 'form', 'predefined' => 'blank_form'];
300 $customForm = Form::resolvePredefinedForm($attributes);
301 $customForm['form_fields'] = json_decode($customForm['form_fields'], true);
302 $customForm['form_fields']['submitButton'] = $customForm['form']['submitButton'];
303 $customForm['form_fields'] = json_encode($customForm['form_fields']);
304 return $customForm;
305 }
306
307 protected function saveForm($formattedInputs, $title, $isStepForm = false, $isConversational = false, $customCss = '')
308 {
309 $customForm = $this->prepareCustomForm($formattedInputs, $isStepForm);
310 $data = Form::prepare($customForm);
311
312 $form = $this->model->create($data);
313 $form->title = $title ?: $form->title . ' (ChatGPT#' . $form->id . ')';
314
315 $formData = (object)$form->toArray();
316 if (FormFieldsParser::hasPaymentFields($formData)) {
317 $form->has_payment = 1;
318 }
319
320 if ($isConversational) {
321 $formMeta = FormMeta::prepare(['type' => 'form', 'predefined' => 'conversational'], $customForm);
322 $form->fill([
323 'form_fields' => Converter::convertExistingForm($form),
324 ])->save();
325 } else {
326 $form->save();
327 $formMeta = FormMeta::prepare(['type' => 'form', 'predefined' => 'blank_form'], $customForm);
328 }
329
330 FormMeta::store($form, $formMeta);
331
332 if ($customCss = fluentformSanitizeCSS($customCss)) {
333 Helper::setFormMeta($form->id, '_custom_form_css', $customCss);
334 }
335
336 do_action('fluentform/inserted_new_form', $form->id, $data);
337 return $form;
338 }
339
340 protected function prepareCustomForm($formattedInputs, $isStepForm)
341 {
342 $formattedInputs = fluentFormSanitizer($formattedInputs);
343 $customForm = $this->getBlankFormConfig();
344 $fields = json_decode($customForm['form_fields'], true);
345
346 $fields['form_fields']['fields'] = $formattedInputs;
347 $fields['form_fields']['submitButton'] = Arr::get($customForm, 'form.submitButton');
348
349 if ($isStepForm) {
350 $fields['form_fields']['stepsWrapper'] = $this->getStepWrapper();
351 }
352
353 $customForm['form_fields'] = json_encode($fields['form_fields']);
354
355 return $customForm;
356 }
357
358 protected function resolveContainerFields($field, $allFields)
359 {
360 $columns = Arr::get($field, 'columns');
361 $columnsCount = count($columns);
362 if (!$columnsCount || $columnsCount > 6) {
363 return [];
364 }
365 $matchedField = Arr::get($allFields, 'container.container_' . $columnsCount . '_col');
366 if (!$matchedField) {
367 return [];
368 }
369 $columnWidth = round(100 / $columnsCount, 2);
370 foreach ($columns as &$column) {
371 $formatedFields = [];
372 $fields = Arr::get($column, 'fields', []);
373 $columnWidth = Arr::get($column, 'width', $columnWidth);
374 foreach ($fields as $colField) {
375 $element = Arr::get($colField, 'element');
376 if ($columnField = $this->processField($element, $colField, $allFields)) {
377 $formatedFields[] = $columnField;
378 }
379 }
380 if ($formatedFields) {
381 $column['fields'] = $formatedFields;
382 $column['width'] = $columnWidth;
383 }
384 }
385 $matchedField['columns'] = $columns;
386 return $matchedField;
387 }
388
389 /**
390 * @return array
391 */
392 protected function getStepWrapper()
393 {
394 return [
395 'stepStart' => [
396 'element' => 'step_start',
397 'attributes' => [
398 'id' => '',
399 'class' => '',
400 ],
401 'settings' => [
402 'progress_indicator' => 'progress-bar',
403 'step_titles' => [],
404 'disable_auto_focus' => 'no',
405 'enable_auto_slider' => 'no',
406 'enable_step_data_persistency' => 'no',
407 'enable_step_page_resume' => 'no',
408 ],
409 'editor_options' => [
410 'title' => 'Start Paging'
411 ],
412 ],
413 'stepEnd' => [
414 'element' => 'step_end',
415 'attributes' => [
416 'id' => '',
417 'class' => '',
418 ],
419 'settings' => [
420 'prev_btn' => [
421 'type' => 'default',
422 'text' => 'Previous',
423 'img_url' => ''
424 ]
425 ],
426 'editor_options' => [
427 'title' => 'End Paging'
428 ],
429 ]
430 ];
431 }
432
433 private function getUserPrompt($args)
434 {
435 $startingQuery = "Create a form for ";
436 $query = Sanitizer::sanitizeTextField(Arr::get($args, 'query'));
437 if (empty($query)) {
438 throw new Exception(esc_html__('Query is empty!', 'fluentform'));
439 }
440
441 // Validate query length to prevent abuse (max 2000 characters)
442 if (strlen($query) > 2000) {
443 throw new Exception(esc_html__('Query is too long. Please limit your prompt to 2000 characters.', 'fluentform'));
444 }
445
446 $additionalQuery = Sanitizer::sanitizeTextField(Arr::get($args, 'additional_query'));
447
448 // Validate additional query length (max 1000 characters)
449 if ($additionalQuery && strlen($additionalQuery) > 1000) {
450 throw new Exception(esc_html__('Additional query is too long. Please limit to 1000 characters.', 'fluentform'));
451 }
452
453 if ($additionalQuery) {
454 $query .= "\n including questions for information like " . $additionalQuery . ".";
455 }
456 return $startingQuery . $query;
457 }
458
459 }
460