WIP
This commit is contained in:
parent
4bb2b59132
commit
f4ab98a2b0
|
@ -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'];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue