This commit is contained in:
Julien Nahum 2023-03-26 12:54:12 +02:00
parent 4bb2b59132
commit f4ab98a2b0
8 changed files with 194 additions and 6 deletions

View File

@ -28,7 +28,7 @@ class GenerateTemplate extends Command
I created a form builder. Forms are represented as Json objects. Here's an example form:
```json
{
"title": "Contact Form",
"title": "Contact Us",
"properties": [
{
"help": null,
@ -219,7 +219,6 @@ class GenerateTemplate extends Command
{
$url = 'https://api.unsplash.com/search/photos?query=' . urlencode($searchQuery) . '&client_id=' . config('services.unslash.access_key');
$response = Http::get($url)->json();
ray($response, $url);
if (isset($response['results'][0]['urls']['regular'])) {
return $response['results'][0]['urls']['regular'];
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\Forms;
use App\Console\Commands\GenerateTemplate;
use App\Http\Controllers\Controller;
use App\Http\Requests\AiGenerateFormRequest;
use App\Service\OpenAi\GptCompleter;
use Illuminate\Support\Str;
class AiFormController extends Controller
{
public function generateForm(AiGenerateFormRequest $request)
{
$this->middleware('throttle:4,1');
$completer = (new GptCompleter(config('services.openai.api_key')))
->setSystemMessage('You are a robot helping to generate forms.');
$completer->completeChat([
["role" => "user", "content" => Str::of(GenerateTemplate::FORM_STRUCTURE_PROMPT)
->replace('[REPLACE]', $request->form_prompt)->toString()]
], 3000);
return $this->success([
'message' => 'Form successfully generated!',
'form' => $this->cleanOutput($completer->getArray())
]);
}
private function cleanOutput($formData)
{
// Add property uuids
foreach ($formData['properties'] as &$property) {
$property['id'] = Str::uuid()->toString();
}
return $formData;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class AiGenerateFormRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'form_prompt' => 'required|string'
];
}
}

View File

@ -0,0 +1,114 @@
<template>
<modal :show="show" @close="$emit('close')" :closeable="!aiForm.busy">
<template #icon>
<template v-if="state=='default'">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-10 h-10 text-blue">
<path fill-rule="evenodd"
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
clip-rule="evenodd"/>
</svg>
</template>
<template v-else-if="state=='ai'">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-blue-500">
<path fill-rule="evenodd"
d="M14.615 1.595a.75.75 0 01.359.852L12.982 9.75h7.268a.75.75 0 01.548 1.262l-10.5 11.25a.75.75 0 01-1.272-.71l1.992-7.302H3.75a.75.75 0 01-.548-1.262l10.5-11.25a.75.75 0 01.913-.143z"
clip-rule="evenodd"/>
</svg>
</template>
</template>
<template #title>
<template v-if="state=='default'">
Choose a base for your form
</template>
<template v-else-if="state=='ai'">
AI-powered form generator
</template>
</template>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8" v-if="state=='default'">
<div class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50"
@click="showInitialFormModal=false">
<div class="p-4">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-blue-500">
<path d="M1.5 8.67v8.58a3 3 0 003 3h15a3 3 0 003-3V8.67l-8.928 5.493a3 3 0 01-3.144 0L1.5 8.67z"/>
<path d="M22.5 6.908V6.75a3 3 0 00-3-3h-15a3 3 0 00-3 3v.158l9.714 5.978a1.5 1.5 0 001.572 0L22.5 6.908z"/>
</svg>
</div>
<p class="font-medium">Start from a simple contact form</p>
</div>
<div v-if="aiFeaturesEnabled" class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50" @click="state='ai'">
<div class="p-4 relative">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-blue-500">
<path fill-rule="evenodd"
d="M14.615 1.595a.75.75 0 01.359.852L12.982 9.75h7.268a.75.75 0 01.548 1.262l-10.5 11.25a.75.75 0 01-1.272-.71l1.992-7.302H3.75a.75.75 0 01-.548-1.262l10.5-11.25a.75.75 0 01.913-.143z"
clip-rule="evenodd"/>
</svg>
</div>
<p class="font-medium text-blue-700">Use our AI to create the form</p>
<span class="text-xs text-gray-500">(1 min)</span>
</div>
<div class="rounded-md border p-4 flex flex-col items-center cursor-pointer hover:bg-gray-50 relative">
<div class="p-4 relative">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 text-blue-500">
<path
d="M11.25 5.337c0-.355-.186-.676-.401-.959a1.647 1.647 0 01-.349-1.003c0-1.036 1.007-1.875 2.25-1.875S15 2.34 15 3.375c0 .369-.128.713-.349 1.003-.215.283-.401.604-.401.959 0 .332.278.598.61.578 1.91-.114 3.79-.342 5.632-.676a.75.75 0 01.878.645 49.17 49.17 0 01.376 5.452.657.657 0 01-.66.664c-.354 0-.675-.186-.958-.401a1.647 1.647 0 00-1.003-.349c-1.035 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401.31 0 .557.262.534.571a48.774 48.774 0 01-.595 4.845.75.75 0 01-.61.61c-1.82.317-3.673.533-5.555.642a.58.58 0 01-.611-.581c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.035-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959a.641.641 0 01-.658.643 49.118 49.118 0 01-4.708-.36.75.75 0 01-.645-.878c.293-1.614.504-3.257.629-4.924A.53.53 0 005.337 15c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.036 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.369 0 .713.128 1.003.349.283.215.604.401.959.401a.656.656 0 00.659-.663 47.703 47.703 0 00-.31-4.82.75.75 0 01.83-.832c1.343.155 2.703.254 4.077.294a.64.64 0 00.657-.642z"/>
</svg>
</div>
<p class="font-medium">Start from a template</p>
<router-link :to="{name:'templates'}" class="absolute inset-0"/>
</div>
</div>
<div v-else-if="state=='ai'">
<a class="absolute top-4 left-4" href="#" @click.prevent="state='default'">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4 inline -mt-1">
<path fill-rule="evenodd" d="M7.72 12.53a.75.75 0 010-1.06l7.5-7.5a.75.75 0 111.06 1.06L9.31 12l6.97 6.97a.75.75 0 11-1.06 1.06l-7.5-7.5z" clip-rule="evenodd" />
</svg>
Back
</a>
<text-area-input label="Form Description" :form="aiForm" name="form_prompt" help="Give us a description of the form you want to build (the more details the better)"
placeholder="A simple contact form, with a name, email and message field" />
<v-button class="w-full" @click.prevent="generateForm" :loading="aiForm.busy">
Generate a form
</v-button>
<p class="text-gray-500 text-xs text-center mt-1">~60 sec</p>
</div>
</modal>
</template>
<script>
import Loader from "../../../common/Loader.vue";
import Form from "vform";
export default {
name: 'CreateFormBaseModal',
components: {Loader},
props: {
show: {type: Boolean, required: true},
},
data: () => ({
state: 'default',
aiForm: new Form({
form_prompt: ''
})
}),
computed: {
aiFeaturesEnabled() {
return window.config.ai_features_enabled
}
},
methods: {
generateForm() {
this.aiForm.post('/api/forms/ai/generate').then(response => {
this.alertSuccess(response.data.message)
this.$emit('form-generated', response.data.form)
this.$emit('close')
}).catch(error => {
this.alertError(error.data.message)
this.state = 'default'
})
}
}
}
</script>

View File

@ -2,6 +2,8 @@
<div class="flex flex-wrap flex-col">
<transition v-if="stateReady" name="fade" mode="out-in">
<div key="2">
<create-form-base-modal @form-generated="formGenerated" :show="showInitialFormModal"
@close="showInitialFormModal=false"/>
<form-editor v-if="!workspacesLoading" ref="editor"
class="w-full flex flex-grow"
:style="{
@ -23,6 +25,7 @@ import Form from 'vform'
import {mapState, mapActions} from 'vuex'
import initForm from "../../mixins/form_editor/initForm.js";
import SeoMeta from '../../mixins/seo-meta.js'
import CreateFormBaseModal from "../../components/pages/forms/create/CreateFormBaseModal.vue"
const loadTemplates = function () {
store.commit('open/templates/startLoading')
@ -35,7 +38,7 @@ export default {
name: 'CreateForm',
mixins: [initForm, SeoMeta],
components: {},
components: {CreateFormBaseModal},
beforeRouteEnter(to, from, next) {
loadTemplates()
@ -50,7 +53,8 @@ export default {
stateReady: false,
loading: false,
error: '',
editorMaxHeight: 500
editorMaxHeight: 500,
showInitialFormModal: false
}
},
@ -92,6 +96,9 @@ export default {
if (template && template.structure) {
this.form = new Form({...this.form.data(), ...template.structure})
}
} else {
// No template loaded, ask how to start
this.showInitialFormModal = true
}
this.closeAlert()
this.loadWorkspaces()
@ -117,6 +124,9 @@ export default {
if (this.$refs.editor) {
this.editorMaxHeight = window.innerHeight - this.$refs.editor.$el.offsetTop
}
},
formGenerated(form) {
this.form = new Form({...this.form.data(), ...form})
}
}
}

View File

@ -16,7 +16,10 @@
>it's free</span>.
</h3>
<div class="mt-6 flex justify-center">
<v-button class="mr-1" :to="{ name: 'forms.create.guest' }" :arrow="true">
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms.create.guest' }" :arrow="true">
Create a form for FREE
</v-button>
<v-button v-else class="mr-1" :to="{ name: 'forms.create' }" :arrow="true">
Create a form for FREE
</v-button>
</div>

View File

@ -13,6 +13,7 @@
'google_analytics_code' => config('services.google_analytics_code'),
'amplitude_code' => config('services.amplitude_code'),
'crisp_website_id' => config('services.crisp_website_id'),
'ai_features_enabled' => !is_null(config('services.openai.api_key'))
];
@endphp
<!DOCTYPE html>

View File

@ -144,6 +144,9 @@ Route::prefix('forms')->name('forms.')->group(function () {
// File uploads
Route::get('assets/{assetFileName}', [PublicFormController::class, 'showAsset'])->name('assets.show');
// AI
Route::post('ai/generate', [\App\Http\Controllers\Forms\AiFormController::class, 'generateForm'])->name('ai.generate');
});
/**
@ -155,4 +158,4 @@ Route::prefix('content')->name('content.')->group(function () {
// Templates
Route::get('templates', [TemplateController::class, 'index'])->name('templates.show');
Route::post('templates', [TemplateController::class, 'create'])->name('templates.create');
Route::post('templates', [TemplateController::class, 'create'])->name('templates.create');