diff --git a/.env.example b/.env.example
index cb01683..46c268d 100644
--- a/.env.example
+++ b/.env.example
@@ -58,3 +58,5 @@ STRIPE_SECRET=
MUX_WORKSPACE_ID=
MUX_API_TOKEN=
+
+OPEN_AI_API_KEY=
diff --git a/app/Console/Commands/GenerateTemplate.php b/app/Console/Commands/GenerateTemplate.php
new file mode 100644
index 0000000..c16e211
--- /dev/null
+++ b/app/Console/Commands/GenerateTemplate.php
@@ -0,0 +1,246 @@
+Looking for a real person to speak to?
We're here for you! Just drop in your queries below and we'll connect with you as soon as we can.
",
+ "re_fillable": false,
+ "use_captcha": false,
+ "redirect_url": null,
+ "submitted_text": "Great, we've received your message. We'll get back to you as soon as we can :)
",
+ "uppercase_labels": false,
+ "submit_button_text": "Submit",
+ "re_fill_button_text": "Fill Again",
+ "color": "#3B82F6"
+ }
+ ```
+ The form properties can have one of the following types: 'text', 'number', 'select', 'multi_select', 'date', 'files', 'checkbox', 'url', 'email', 'phone_number'.
+ All form properties objects need to have the keys 'help', 'name', 'type', 'hidden', 'placeholder', 'prefill'.
+
+ For the type "select" and "multi_select", the input object must have a key "select" (or "multi_select") that's mapped to an object like this one:
+ ```json
+ {
+ "options": [
+ {"id":"Option 1","name":"Option 1"},
+ {"id":"Pption 2","name":"Option 2"}
+ ]
+ }
+ ```
+
+ For the type "number" you can set the property "is_rating" to "true" to turn it into a star rating input.
+
+ If the form is too long, you can paginate it by adding a page break block in the list of properties:
+ ```json
+ {
+ "name":"Page Break",
+ "next_btn_text":"Next",
+ "previous_btn_text":"Previous",
+ "type":"nf-page-break",
+ }
+ ```
+
+ Give me the JSON code only, for the following form: "[REPLACE]"
+ Do not ask me for more information about required properties or types, suggest me a form structure instead.
+ EOD;
+
+ const FORM_DESCRIPTION_PROMPT = <<setSystemMessage('You are a robot helping to generate forms.');
+ $completer->completeChat([
+ ["role" => "user", "content" => Str::of(self::FORM_STRUCTURE_PROMPT)->replace('[REPLACE]', $this->argument('prompt'))->toString()]
+ ], 3000);
+ $formData = $completer->getArray();
+
+ // Now get description and QAs
+ $formDescriptionPrompt = Str::of(self::FORM_DESCRIPTION_PROMPT)->replace('[REPLACE]', $this->argument('prompt'))->toString();
+ $formDescription = $completer->completeChat([
+ ["role" => "user", "content" => $formDescriptionPrompt]
+ ])->getString();
+ $formQAs = $completer->completeChat([
+ ["role" => "user", "content" => $formDescriptionPrompt],
+ ["role" => "assistant", "content" => $formDescription],
+ ["role" => "user", "content" => self::FORM_QAS_PROMPT]
+ ])->getArray();
+ $formTitle = $completer->completeChat([
+ ["role" => "user", "content" => $formDescriptionPrompt],
+ ["role" => "assistant", "content" => $formDescription],
+ ["role" => "user", "content" => self::FORM_TITLE_PROMPT]
+ ])->getString();
+
+ // Finally get keyworks for image cover
+ $formCoverKeyworks = $completer->completeChat([
+ ["role" => "user", "content" => $formDescriptionPrompt],
+ ["role" => "assistant", "content" => $formDescription],
+ ["role" => "user", "content" => self::FORM_IMG_KEYWORDS_PROMPT]
+ ])->getArray();
+ $imageUrl = $this->getImageCoverUrl($formCoverKeyworks['search_query']);
+
+ $template = $this->createFormTemplate($formData, $formTitle, $formDescription, $formQAs, $imageUrl);
+ $this->info('/templates/' . $template->slug);
+
+ return Command::SUCCESS;
+ }
+
+ /**
+ * Get an image cover URL for the template using unsplash API
+ */
+ private function getImageCoverUrl($searchQuery): ?string
+ {
+ $url = 'https://api.unsplash.com/search/photos?query=' . urlencode($searchQuery) . '&client_id=' . config('services.unslash.access_key');
+ $response = Http::get($url)->json();
+ if (isset($response['results'][0]['urls']['regular'])) {
+ return $response['results'][0]['urls']['regular'];
+ }
+ return null;
+ }
+
+ private function createFormTemplate(array $formData, string $formTitle, string $formDescription, array $formQAs, ?string $imageUrl)
+ {
+ // Add property uuids
+ foreach ($formData['properties'] as &$property) {
+ $property['id'] = Str::uuid()->toString();
+ }
+
+ // Clean data
+ $formTitle = Str::of($formTitle)->replace('"', '')->toString();
+
+ return Template::create([
+ 'name' => $formTitle,
+ 'description' => $formDescription,
+ 'questions' => $formQAs,
+ 'structure' => $formData,
+ 'image_url' => $imageUrl,
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Forms/AiFormController.php b/app/Http/Controllers/Forms/AiFormController.php
new file mode 100644
index 0000000..aa371fb
--- /dev/null
+++ b/app/Http/Controllers/Forms/AiFormController.php
@@ -0,0 +1,38 @@
+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;
+ }
+}
diff --git a/app/Http/Controllers/SpaController.php b/app/Http/Controllers/SpaController.php
index eb4ce03..50456f1 100644
--- a/app/Http/Controllers/SpaController.php
+++ b/app/Http/Controllers/SpaController.php
@@ -8,8 +8,6 @@ class SpaController extends Controller
{
/**
* Get the SPA view.
- *
- * @return \Illuminate\Http\Response
*/
public function __invoke(Request $request)
{
diff --git a/app/Http/Requests/AiGenerateFormRequest.php b/app/Http/Requests/AiGenerateFormRequest.php
new file mode 100644
index 0000000..6aac735
--- /dev/null
+++ b/app/Http/Requests/AiGenerateFormRequest.php
@@ -0,0 +1,20 @@
+
+ */
+ public function rules()
+ {
+ return [
+ 'form_prompt' => 'required|string'
+ ];
+ }
+}
diff --git a/app/Models/Template.php b/app/Models/Template.php
index cbf541b..db3d652 100644
--- a/app/Models/Template.php
+++ b/app/Models/Template.php
@@ -4,11 +4,13 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
+use Spatie\Sluggable\HasSlug;
+use Spatie\Sluggable\SlugOptions;
use Stevebauman\Purify\Facades\Purify;
class Template extends Model
{
- use HasFactory;
+ use HasFactory, HasSlug;
protected $fillable = [
'name',
@@ -29,4 +31,15 @@ class Template extends Model
// Strip out unwanted html
$this->attributes['description'] = Purify::clean($value);
}
+
+ /**
+ * Config/options
+ */
+ public function getSlugOptions(): SlugOptions
+ {
+ return SlugOptions::create()
+ ->doNotGenerateSlugsOnUpdate()
+ ->generateSlugsFrom('name')
+ ->saveSlugsTo('slug');
+ }
}
diff --git a/app/Service/OpenAi/GptCompleter.php b/app/Service/OpenAi/GptCompleter.php
new file mode 100644
index 0000000..797c8c9
--- /dev/null
+++ b/app/Service/OpenAi/GptCompleter.php
@@ -0,0 +1,126 @@
+openAi = \OpenAI::client($apiKey);
+ }
+
+ public function setSystemMessage(string $systemMessage): self
+ {
+ $this->systemMessage = $systemMessage;
+ return $this;
+ }
+
+ public function completeChat(array $messages, int $maxTokens = 512, float $temperature = 0.81): self
+ {
+ $this->computeChatCompletion($messages, $maxTokens, $temperature)
+ ->queryCompletion();
+
+ return $this;
+ }
+
+ public function getBool(): bool
+ {
+ switch (strtolower($this->result)) {
+ case 'true':
+ return true;
+ case 'false':
+ return false;
+ default:
+ throw new \InvalidArgumentException("Expected a boolean value, got {$this->result}");
+ }
+ }
+
+ public function getArray(): array
+ {
+ $payload = Str::of($this->result)->trim();
+ if ($payload->contains('```json')) {
+ $payload = $payload->after('```json')->before('```');
+ } else if ($payload->contains('```')) {
+ $payload = $payload->after('```')->before('```');
+ }
+ $payload = $payload->toString();
+ $exception = null;
+
+ for ($i = 0; $i < $this->retries; $i++) {
+ try {
+ $payload = (new JsonFixer)->fix($payload);
+ return json_decode($payload, true);
+ } catch (\Aws\Exception\InvalidJsonException $e) {
+ $exception = $e;
+ Log::warning("Invalid JSON, retrying:");
+ Log::warning($payload);
+ Log::warning(json_encode($this->completionInput));
+ $this->queryCompletion();
+ }
+ }
+ throw $exception;
+ }
+
+ public function getString(): string
+ {
+ return trim($this->result);
+ }
+
+ public function getTokenUsed(): int
+ {
+ return $this->tokenUsed;
+ }
+
+ protected function computeChatCompletion(array $messages, int $maxTokens = 512, float $temperature = 0.81): self
+ {
+ if (isset($this->systemMessage) && $messages[0]['role'] !== 'system') {
+ $messages = array_merge([[
+ 'role' => 'system',
+ 'content' => $this->systemMessage
+ ]], $messages);
+ }
+
+ $completionInput = [
+ 'model' => self::AI_MODEL,
+ 'messages' => $messages,
+ 'max_tokens' => $maxTokens,
+ 'temperature' => $temperature
+ ];
+
+ $this->completionInput = $completionInput;
+ return $this;
+ }
+
+ protected function queryCompletion(): self {
+ try {
+ Log::debug("Open AI query: " . json_encode($this->completionInput));
+ $response = $this->openAi->chat()->create($this->completionInput);
+ } catch (ErrorException $errorException) {
+ // Retry once
+ Log::warning("Open AI error, retrying: {$errorException->getMessage()}");
+ $response = $this->openAi->chat()->create($this->completionInput);
+ }
+ $this->tokenUsed += $response->usage->totalTokens;
+ $this->result = $response->choices[0]->message->content;
+ return $this;
+ }
+}
diff --git a/app/Service/OpenAi/Utils/JsonFixer.php b/app/Service/OpenAi/Utils/JsonFixer.php
new file mode 100644
index 0000000..f65cfa8
--- /dev/null
+++ b/app/Service/OpenAi/Utils/JsonFixer.php
@@ -0,0 +1,272 @@
+
+ *
+ *
+ * Licensed under MIT license.
+ */
+
+use App\Exceptions\Coursework\InvalidJsonException;
+
+/**
+ * Attempts to fix truncated JSON by padding contextual counterparts at the end.
+ *
+ * @author Jitendra Adhikari
+ * @license MIT
+ *
+ * @link https://github.com/adhocore/php-json-fixer
+ */
+class JsonFixer
+{
+ use PadsJson;
+
+ /** @var array Current token stack indexed by position */
+ protected $stack = [];
+
+ /** @var bool If current char is within a string */
+ protected $inStr = false;
+
+ /** @var bool Whether to throw Exception on failure */
+ protected $silent = false;
+
+ /** @var array The complementary pairs */
+ protected $pairs = [
+ '{' => '}',
+ '[' => ']',
+ '"' => '"',
+ ];
+
+ /** @var int The last seen object `{` type position */
+ protected $objectPos = -1;
+
+ /** @var int The last seen array `[` type position */
+ protected $arrayPos = -1;
+
+ /** @var string Missing value. (Options: true, false, null) */
+ protected $missingValue = 'null';
+
+ /**
+ * Set/unset silent mode.
+ *
+ * @param bool $silent
+ *
+ * @return $this
+ */
+ public function silent($silent = true)
+ {
+ $this->silent = (bool)$silent;
+
+ return $this;
+ }
+
+ /**
+ * Set missing value.
+ *
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function missingValue($value)
+ {
+ if ($value === null) {
+ $value = 'null';
+ } elseif (\is_bool($value)) {
+ $value = $value ? 'true' : 'false';
+ }
+
+ $this->missingValue = $value;
+
+ return $this;
+ }
+
+ /**
+ * Fix the truncated JSON.
+ *
+ * @param string $json The JSON string to fix.
+ *
+ * @return string Fixed JSON. If failed with silent then original JSON.
+ * @throws InvalidJsonException When fixing fails.
+ *
+ */
+ public function fix($json)
+ {
+ list($head, $json, $tail) = $this->trim($json);
+
+ if (empty($json) || $this->isValid($json)) {
+ return $json;
+ }
+
+ if (null !== $tmpJson = $this->quickFix($json)) {
+ return $tmpJson;
+ }
+
+ $this->reset();
+
+ return $head . $this->doFix($json) . $tail;
+ }
+
+ protected function trim($json)
+ {
+ \preg_match('/^(\s*)([^\s]+)(\s*)$/', $json, $match);
+
+ $match += ['', '', '', ''];
+ $match[2] = \trim($json);
+
+ \array_shift($match);
+
+ return $match;
+ }
+
+ protected function isValid($json)
+ {
+ \json_decode($json);
+
+ return \JSON_ERROR_NONE === \json_last_error();
+ }
+
+ protected function quickFix($json)
+ {
+ if (\strlen($json) === 1 && isset($this->pairs[$json])) {
+ return $json . $this->pairs[$json];
+ }
+
+ if ($json[0] !== '"') {
+ return $this->maybeLiteral($json);
+ }
+
+ return $this->padString($json);
+ }
+
+ protected function reset()
+ {
+ $this->stack = [];
+ $this->inStr = false;
+ $this->objectPos = -1;
+ $this->arrayPos = -1;
+ }
+
+ protected function maybeLiteral($json)
+ {
+ if (!\in_array($json[0], ['t', 'f', 'n'])) {
+ return null;
+ }
+
+ foreach (['true', 'false', 'null'] as $literal) {
+ if (\strpos($literal, $json) === 0) {
+ return $literal;
+ }
+ }
+
+ // @codeCoverageIgnoreStart
+ return null;
+ // @codeCoverageIgnoreEnd
+ }
+
+ protected function doFix($json)
+ {
+ list($index, $char) = [-1, ''];
+
+ while (isset($json[++$index])) {
+ list($prev, $char) = [$char, $json[$index]];
+
+ $next = isset($json[$index + 1]) ? $json[$index + 1] : '';
+
+ if (!\in_array($char, [' ', "\n", "\r"])) {
+ $this->stack($prev, $char, $index, $next);
+ }
+ }
+
+ return $this->fixOrFail($json);
+ }
+
+ protected function stack($prev, $char, $index, $next)
+ {
+ if ($this->maybeStr($prev, $char, $index)) {
+ return;
+ }
+
+ $last = $this->lastToken();
+
+ if (\in_array($last, [',', ':', '"']) && \preg_match('/\"|\d|\{|\[|t|f|n/', $char)) {
+ $this->popToken();
+ }
+
+ if (\in_array($char, [',', ':', '[', '{'])) {
+ $this->stack[$index] = $char;
+ }
+
+ $this->updatePos($char, $index);
+ }
+
+ protected function lastToken()
+ {
+ return \end($this->stack);
+ }
+
+ protected function popToken($token = null)
+ {
+ // Last one
+ if (null === $token) {
+ return \array_pop($this->stack);
+ }
+
+ $keys = \array_reverse(\array_keys($this->stack));
+ foreach ($keys as $key) {
+ if ($this->stack[$key] === $token) {
+ unset($this->stack[$key]);
+ break;
+ }
+ }
+ }
+
+ protected function maybeStr($prev, $char, $index)
+ {
+ if ($prev !== '\\' && $char === '"') {
+ $this->inStr = !$this->inStr;
+ }
+
+ if ($this->inStr && $this->lastToken() !== '"') {
+ $this->stack[$index] = '"';
+ }
+
+ return $this->inStr;
+ }
+
+ protected function updatePos($char, $index)
+ {
+ if ($char === '{') {
+ $this->objectPos = $index;
+ } elseif ($char === '}') {
+ $this->popToken('{');
+ $this->objectPos = -1;
+ } elseif ($char === '[') {
+ $this->arrayPos = $index;
+ } elseif ($char === ']') {
+ $this->popToken('[');
+ $this->arrayPos = -1;
+ }
+ }
+
+ protected function fixOrFail($json)
+ {
+ $length = \strlen($json);
+ $tmpJson = $this->pad($json);
+
+ if ($this->isValid($tmpJson)) {
+ return $tmpJson;
+ }
+
+ if ($this->silent) {
+ return $json;
+ }
+
+ throw new InvalidJsonException(
+ \sprintf('Could not fix JSON (tried padding `%s`)', \substr($tmpJson, $length), $json)
+ );
+ }
+}
diff --git a/app/Service/OpenAi/Utils/PadsJson.php b/app/Service/OpenAi/Utils/PadsJson.php
new file mode 100644
index 0000000..6d17c42
--- /dev/null
+++ b/app/Service/OpenAi/Utils/PadsJson.php
@@ -0,0 +1,113 @@
+
+ * @license MIT
+ *
+ * @internal
+ *
+ * @link https://github.com/adhocore/php-json-fixer
+ */
+trait PadsJson
+{
+ public function pad($tmpJson)
+ {
+ if (!$this->inStr) {
+ $tmpJson = \rtrim($tmpJson, ',');
+ while ($this->lastToken() === ',') {
+ $this->popToken();
+ }
+ }
+
+ $tmpJson = $this->padLiteral($tmpJson);
+ $tmpJson = $this->padObject($tmpJson);
+
+ return $this->padStack($tmpJson);
+ }
+
+ protected function padLiteral($tmpJson)
+ {
+ if ($this->inStr) {
+ return $tmpJson;
+ }
+
+ $match = \preg_match('/(tr?u?e?|fa?l?s?e?|nu?l?l?)$/', $tmpJson, $matches);
+
+ if (!$match || null === $literal = $this->maybeLiteral($matches[1])) {
+ return $tmpJson;
+ }
+
+ return \substr($tmpJson, 0, 0 - \strlen($matches[1])) . $literal;
+ }
+
+ protected function padStack($tmpJson)
+ {
+ foreach (\array_reverse($this->stack, true) as $token) {
+ if (isset($this->pairs[$token])) {
+ $tmpJson .= $this->pairs[$token];
+ }
+ }
+
+ return $tmpJson;
+ }
+
+ protected function padObject($tmpJson)
+ {
+ if (!$this->objectNeedsPadding($tmpJson)) {
+ return $tmpJson;
+ }
+
+ $part = \substr($tmpJson, $this->objectPos + 1);
+ if (\preg_match('/(\s*\"[^"]+\"\s*:\s*[^,]+,?)+$/', $part, $matches)) {
+ return $tmpJson;
+ }
+
+ if ($this->inStr) {
+ $tmpJson .= '"';
+ }
+
+ $tmpJson = $this->padIf($tmpJson, ':');
+ $tmpJson = $tmpJson . $this->missingValue;
+
+ if ($this->lastToken() === '"') {
+ $this->popToken();
+ }
+
+ return $tmpJson;
+ }
+
+ protected function objectNeedsPadding($tmpJson)
+ {
+ $last = \substr($tmpJson, -1);
+ $empty = $last === '{' && !$this->inStr;
+
+ return !$empty && $this->arrayPos < $this->objectPos;
+ }
+
+ protected function padString($string)
+ {
+ $last = \substr($string, -1);
+ $last2 = \substr($string, -2);
+
+ if ($last2 === '\"' || $last !== '"') {
+ return $string . '"';
+ }
+
+ // @codeCoverageIgnoreStart
+ return null;
+ // @codeCoverageIgnoreEnd
+ }
+
+ protected function padIf($string, $substr)
+ {
+ if (\substr($string, 0 - \strlen($substr)) !== $substr) {
+ return $string . $substr;
+ }
+
+ return $string;
+ }
+}
diff --git a/app/Service/SeoMetaResolver.php b/app/Service/SeoMetaResolver.php
index 7b415ae..5232562 100644
--- a/app/Service/SeoMetaResolver.php
+++ b/app/Service/SeoMetaResolver.php
@@ -178,7 +178,7 @@ class SeoMetaResolver
return [
'title' => $template->name . $this->titleSuffix(),
- 'description' => Str::of($template->description)->limit(160) ,
+ 'description' => Str::of($template->description)->limit(160),
'image' => $template->image_url
];
}
diff --git a/composer.json b/composer.json
index 28ac9a2..9d4cd64 100644
--- a/composer.json
+++ b/composer.json
@@ -36,6 +36,7 @@
"laravel/vapor-ui": "^1.5",
"league/flysystem-aws-s3-v3": "^3.0",
"maatwebsite/excel": "^3.1",
+ "openai-php/client": "^0.3.5",
"sentry/sentry-laravel": "^2.11.0",
"spatie/laravel-sitemap": "^6.0",
"spatie/laravel-sluggable": "^3.0",
diff --git a/composer.lock b/composer.lock
index 8858df1..ff1f0c4 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "417c01c6f73f7e870992a7d71e75f363",
+ "content-hash": "54e1e706f48dc10e931a4411e310be5c",
"packages": [
{
"name": "asm89/stack-cors",
@@ -4972,6 +4972,87 @@
],
"time": "2022-06-22T07:13:36+00:00"
},
+ {
+ "name": "openai-php/client",
+ "version": "v0.3.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/openai-php/client.git",
+ "reference": "0bb01e93fbd1f155af1c5a70caa53252dd13478f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/openai-php/client/zipball/0bb01e93fbd1f155af1c5a70caa53252dd13478f",
+ "reference": "0bb01e93fbd1f155af1c5a70caa53252dd13478f",
+ "shasum": ""
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^7.5.0",
+ "php": "^8.1.0"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.6.0",
+ "nunomaduro/collision": "^7.0.5",
+ "pestphp/pest": "^2.0.0",
+ "pestphp/pest-plugin-arch": "^2.0.0",
+ "pestphp/pest-plugin-mock": "^2.0.0",
+ "phpstan/phpstan": "^1.10.3",
+ "rector/rector": "^0.14.8",
+ "symfony/var-dumper": "^6.2.7"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/OpenAI.php"
+ ],
+ "psr-4": {
+ "OpenAI\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "OpenAI PHP is a supercharged PHP API client that allows you to interact with the Open AI API",
+ "keywords": [
+ "GPT-3",
+ "api",
+ "client",
+ "codex",
+ "dall-e",
+ "language",
+ "natural",
+ "openai",
+ "php",
+ "processing",
+ "sdk"
+ ],
+ "support": {
+ "issues": "https://github.com/openai-php/client/issues",
+ "source": "https://github.com/openai-php/client/tree/v0.3.5"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.com/paypalme/enunomaduro",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/gehrisandro",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nunomaduro",
+ "type": "github"
+ }
+ ],
+ "time": "2023-03-08T06:39:38+00:00"
+ },
{
"name": "phenx/php-font-lib",
"version": "0.5.4",
diff --git a/config/services.php b/config/services.php
index 403502f..4120b72 100644
--- a/config/services.php
+++ b/config/services.php
@@ -48,6 +48,15 @@ return [
'worker' => env('NOTION_WORKER','https://notion-forms-worker.notionforms.workers.dev/v1')
],
+ 'openai' => [
+ 'api_key' => env('OPEN_AI_API_KEY'),
+ ],
+
+ 'unslash' => [
+ 'access_key' => env('UNSPLASH_ACCESS_KEY'),
+ 'secret_key' => env('UNSPLASH_SECRET_KEY'),
+ ],
+
'google_analytics_code' => env('GOOGLE_ANALYTICS_CODE'),
'amplitude_code' => env('AMPLITUDE_CODE'),
'crisp_website_id' => env('CRISP_WEBSITE_ID'),
diff --git a/database/migrations/2023_03_14_094623_change_templates_image_url_to_nullable.php b/database/migrations/2023_03_14_094623_change_templates_image_url_to_nullable.php
new file mode 100644
index 0000000..8dbe017
--- /dev/null
+++ b/database/migrations/2023_03_14_094623_change_templates_image_url_to_nullable.php
@@ -0,0 +1,33 @@
+string('image_url')->nullable()->change();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('templates', function (Blueprint $table) {
+ $table->string('image_url')->nullable(false)->change();
+ });
+ }
+};
diff --git a/resources/js/components/open/forms/OpenForm.vue b/resources/js/components/open/forms/OpenForm.vue
index 13300c1..f423b60 100644
--- a/resources/js/components/open/forms/OpenForm.vue
+++ b/resources/js/components/open/forms/OpenForm.vue
@@ -23,7 +23,7 @@
class="nf-code w-full px-2 mb-3"
v-html="field.content"
/>
-
+
+
+
+
+
+
+
+
+
+
+
+ Choose a base for your form
+
+
+ AI-powered form generator
+
+
+
+
+
+
Start from a simple contact form
+
+
+
+
Use our AI to create the form
+
(1 min)
+
+
+
+
Start from a template
+
+
+
+
+
+
+
+
diff --git a/resources/js/pages/forms/create.vue b/resources/js/pages/forms/create.vue
index 76c626c..ef21c5a 100644
--- a/resources/js/pages/forms/create.vue
+++ b/resources/js/pages/forms/create.vue
@@ -2,6 +2,8 @@
+
Template Preview
+ class="mb-4 p-4 bg-gray-50 rounded-lg"/>
Frequently asked questions
-
{{ ques.question }}
-
+
{{ ques.question }}
+
diff --git a/resources/js/pages/welcome.vue b/resources/js/pages/welcome.vue
index 9b95a43..b146b57 100644
--- a/resources/js/pages/welcome.vue
+++ b/resources/js/pages/welcome.vue
@@ -16,7 +16,10 @@
>it's free.
-
+
+ Create a form for FREE
+
+
Create a form for FREE
diff --git a/resources/views/spa.blade.php b/resources/views/spa.blade.php
index 7a369f2..f9dc12d 100644
--- a/resources/views/spa.blade.php
+++ b/resources/views/spa.blade.php
@@ -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
diff --git a/routes/api.php b/routes/api.php
index da24172..90968fe 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -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');
\ No newline at end of file
+Route::post('templates', [TemplateController::class, 'create'])->name('templates.create');