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:
|
I created a form builder. Forms are represented as Json objects. Here's an example form:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"title": "Contact Form",
|
"title": "Contact Us",
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"help": null,
|
"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');
|
$url = 'https://api.unsplash.com/search/photos?query=' . urlencode($searchQuery) . '&client_id=' . config('services.unslash.access_key');
|
||||||
$response = Http::get($url)->json();
|
$response = Http::get($url)->json();
|
||||||
ray($response, $url);
|
|
||||||
if (isset($response['results'][0]['urls']['regular'])) {
|
if (isset($response['results'][0]['urls']['regular'])) {
|
||||||
return $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">
|
<div class="flex flex-wrap flex-col">
|
||||||
<transition v-if="stateReady" name="fade" mode="out-in">
|
<transition v-if="stateReady" name="fade" mode="out-in">
|
||||||
<div key="2">
|
<div key="2">
|
||||||
|
<create-form-base-modal @form-generated="formGenerated" :show="showInitialFormModal"
|
||||||
|
@close="showInitialFormModal=false"/>
|
||||||
<form-editor v-if="!workspacesLoading" ref="editor"
|
<form-editor v-if="!workspacesLoading" ref="editor"
|
||||||
class="w-full flex flex-grow"
|
class="w-full flex flex-grow"
|
||||||
:style="{
|
:style="{
|
||||||
|
@ -23,6 +25,7 @@ import Form from 'vform'
|
||||||
import {mapState, mapActions} from 'vuex'
|
import {mapState, mapActions} from 'vuex'
|
||||||
import initForm from "../../mixins/form_editor/initForm.js";
|
import initForm from "../../mixins/form_editor/initForm.js";
|
||||||
import SeoMeta from '../../mixins/seo-meta.js'
|
import SeoMeta from '../../mixins/seo-meta.js'
|
||||||
|
import CreateFormBaseModal from "../../components/pages/forms/create/CreateFormBaseModal.vue"
|
||||||
|
|
||||||
const loadTemplates = function () {
|
const loadTemplates = function () {
|
||||||
store.commit('open/templates/startLoading')
|
store.commit('open/templates/startLoading')
|
||||||
|
@ -35,7 +38,7 @@ export default {
|
||||||
name: 'CreateForm',
|
name: 'CreateForm',
|
||||||
|
|
||||||
mixins: [initForm, SeoMeta],
|
mixins: [initForm, SeoMeta],
|
||||||
components: {},
|
components: {CreateFormBaseModal},
|
||||||
|
|
||||||
beforeRouteEnter(to, from, next) {
|
beforeRouteEnter(to, from, next) {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
|
@ -50,7 +53,8 @@ export default {
|
||||||
stateReady: false,
|
stateReady: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: '',
|
error: '',
|
||||||
editorMaxHeight: 500
|
editorMaxHeight: 500,
|
||||||
|
showInitialFormModal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -92,6 +96,9 @@ export default {
|
||||||
if (template && template.structure) {
|
if (template && template.structure) {
|
||||||
this.form = new Form({...this.form.data(), ...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.closeAlert()
|
||||||
this.loadWorkspaces()
|
this.loadWorkspaces()
|
||||||
|
@ -117,6 +124,9 @@ export default {
|
||||||
if (this.$refs.editor) {
|
if (this.$refs.editor) {
|
||||||
this.editorMaxHeight = window.innerHeight - this.$refs.editor.$el.offsetTop
|
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>.
|
>it's free</span>.
|
||||||
</h3>
|
</h3>
|
||||||
<div class="mt-6 flex justify-center">
|
<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
|
Create a form for FREE
|
||||||
</v-button>
|
</v-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
'google_analytics_code' => config('services.google_analytics_code'),
|
'google_analytics_code' => config('services.google_analytics_code'),
|
||||||
'amplitude_code' => config('services.amplitude_code'),
|
'amplitude_code' => config('services.amplitude_code'),
|
||||||
'crisp_website_id' => config('services.crisp_website_id'),
|
'crisp_website_id' => config('services.crisp_website_id'),
|
||||||
|
'ai_features_enabled' => !is_null(config('services.openai.api_key'))
|
||||||
];
|
];
|
||||||
@endphp
|
@endphp
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
|
@ -144,6 +144,9 @@ Route::prefix('forms')->name('forms.')->group(function () {
|
||||||
|
|
||||||
// File uploads
|
// File uploads
|
||||||
Route::get('assets/{assetFileName}', [PublicFormController::class, 'showAsset'])->name('assets.show');
|
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
|
// Templates
|
||||||
Route::get('templates', [TemplateController::class, 'index'])->name('templates.show');
|
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');
|
||||||
|
|
Loading…
Reference in New Issue