diff --git a/api/app/Console/Commands/GenerateTemplate.php b/api/app/Console/Commands/GenerateTemplate.php index 71b45114..4800ab2c 100644 --- a/api/app/Console/Commands/GenerateTemplate.php +++ b/api/app/Console/Commands/GenerateTemplate.php @@ -2,12 +2,8 @@ namespace App\Console\Commands; -use App\Models\Template; -use App\Service\AI\Prompts\Form\GenerateFormPrompt; -use App\Service\AI\Prompts\Template\GenerateTemplateMetadataPrompt; +use App\Jobs\Template\GenerateTemplateJob; use Illuminate\Console\Command; -use Illuminate\Support\Facades\Http; -use Illuminate\Support\Str; class GenerateTemplate extends Command { @@ -25,124 +21,26 @@ class GenerateTemplate extends Command */ protected $description = 'Generates a new form template from a prompt'; - public const MAX_RELATED_TEMPLATES = 8; + public $generatedTemplate; /** - * Execute the console command. + * Execute the command. * * @return int */ public function handle() { - // Get form structure using the form prompt class - $formData = GenerateFormPrompt::run($this->argument('prompt')); + $job = new GenerateTemplateJob($this->argument('prompt')); + $job->handle(); - // Generate all template metadata using the consolidated prompt - $metadata = GenerateTemplateMetadataPrompt::run($this->argument('prompt')); + $this->generatedTemplate = $job->generatedTemplate; - // Extract metadata components - $formShortDescription = $metadata['short_description']; - $formDescription = $metadata['detailed_description']; - $formTitle = $metadata['title']; - $industry = $metadata['industries']; - $types = $metadata['types']; - $formQAs = $metadata['qa_content']; - - // Get Related Templates - $relatedTemplates = $this->getRelatedTemplates($industry, $types); - - // Get image cover URL - $imageUrl = $this->getImageCoverUrl($metadata['image_search_query']); - - $template = $this->createFormTemplate( - $formData, - $formTitle, - $formDescription, - $formShortDescription, - $formQAs, - $imageUrl, - $industry, - $types, - $relatedTemplates - ); - - // Set reverse related Templates - $this->setReverseRelatedTemplates($template); - - $this->info(front_url('/form-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.unsplash.access_key'); - $response = Http::get($url)->json(); - $photoIndex = rand(0, max(count($response['results']) - 1, 10)); - if (isset($response['results'][$photoIndex]['urls']['regular'])) { - return Str::of($response['results'][$photoIndex]['urls']['regular'])->replace('w=1080', 'w=600')->toString(); + if ($this->generatedTemplate) { + $this->info(front_url('/templates/' . $this->generatedTemplate->slug)); + return Command::SUCCESS; } - return null; - } - - private function getRelatedTemplates(array $industries, array $types): array - { - $templateScore = []; - Template::chunk(100, function ($otherTemplates) use ($industries, $types, &$templateScore) { - foreach ($otherTemplates as $otherTemplate) { - $industryOverlap = count(array_intersect($industries ?? [], $otherTemplate->industry ?? [])); - $typeOverlap = count(array_intersect($types ?? [], $otherTemplate->types ?? [])); - $score = $industryOverlap + $typeOverlap; - if ($score > 1) { - $templateScore[$otherTemplate->slug] = $score; - } - } - }); - arsort($templateScore); // Sort by Score - - return array_slice(array_keys($templateScore), 0, self::MAX_RELATED_TEMPLATES); - } - - private function createFormTemplate( - array $formData, - string $formTitle, - string $formDescription, - string $formShortDescription, - array $formQAs, - ?string $imageUrl, - array $industry, - array $types, - array $relatedTemplates - ) { - return Template::create([ - 'name' => $formTitle, - 'description' => $formDescription, - 'short_description' => $formShortDescription, - 'questions' => $formQAs, - 'structure' => $formData, - 'image_url' => $imageUrl, - 'publicly_listed' => true, - 'industries' => $industry, - 'types' => $types, - 'related_templates' => $relatedTemplates, - ]); - } - - private function setReverseRelatedTemplates(Template $newTemplate) - { - if (!$newTemplate || count($newTemplate->related_templates) === 0) { - return; - } - - $templates = Template::whereIn('slug', $newTemplate->related_templates)->get(); - foreach ($templates as $template) { - if (count($template->related_templates) < self::MAX_RELATED_TEMPLATES) { - $template->update(['related_templates' => array_merge($template->related_templates, [$newTemplate->slug])]); - } - } + $this->error('Failed to generate template'); + return Command::FAILURE; } } diff --git a/api/app/Http/Controllers/Admin/AdminController.php b/api/app/Http/Controllers/Admin/AdminController.php index 657e8461..0902bde8 100644 --- a/api/app/Http/Controllers/Admin/AdminController.php +++ b/api/app/Http/Controllers/Admin/AdminController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Jobs\Template\GenerateTemplateJob; use App\Models\Forms\Form; use App\Models\User; use Illuminate\Http\Request; @@ -18,6 +19,20 @@ class AdminController extends Controller $this->middleware('moderator'); } + public function createTemplate(Request $request) + { + $request->validate([ + 'template_prompt' => 'required|string|max:4000' + ]); + + $job = new GenerateTemplateJob($request->template_prompt); + $job->handle(); + + return $this->success([ + 'template_slug' => $job->generatedTemplate?->slug ?? null + ]); + } + public function fetchUser($identifier) { $user = null; diff --git a/api/app/Jobs/Template/GenerateTemplateJob.php b/api/app/Jobs/Template/GenerateTemplateJob.php new file mode 100644 index 00000000..6167e849 --- /dev/null +++ b/api/app/Jobs/Template/GenerateTemplateJob.php @@ -0,0 +1,146 @@ +prompt); + + // Generate all template metadata using the consolidated prompt + $metadata = GenerateTemplateMetadataPrompt::run($this->prompt); + + // Extract metadata components + $formShortDescription = $metadata['short_description']; + $formDescription = $metadata['detailed_description']; + $formTitle = $metadata['title']; + $industry = $metadata['industries']; + $types = $metadata['types']; + $formQAs = $metadata['qa_content']; + + // Get Related Templates + $relatedTemplates = $this->getRelatedTemplates($industry, $types); + + // Get image cover URL + $imageUrl = $this->getImageCoverUrl($metadata['image_search_query']); + + $template = $this->createFormTemplate( + $formData, + $formTitle, + $formDescription, + $formShortDescription, + $formQAs, + $imageUrl, + $industry, + $types, + $relatedTemplates + ); + $this->generatedTemplate = $template; + + // Set reverse related Templates + $this->setReverseRelatedTemplates($template); + } + + /** + * 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.unsplash.access_key'); + $response = Http::get($url)->json(); + $photoIndex = rand(0, max(count($response['results']) - 1, 10)); + if (isset($response['results'][$photoIndex]['urls']['regular'])) { + return Str::of($response['results'][$photoIndex]['urls']['regular'])->replace('w=1080', 'w=600')->toString(); + } + + return null; + } + + private function getRelatedTemplates(array $industries, array $types): array + { + $templateScore = []; + Template::chunk(100, function ($otherTemplates) use ($industries, $types, &$templateScore) { + foreach ($otherTemplates as $otherTemplate) { + $industryOverlap = count(array_intersect($industries ?? [], $otherTemplate->industry ?? [])); + $typeOverlap = count(array_intersect($types ?? [], $otherTemplate->types ?? [])); + $score = $industryOverlap + $typeOverlap; + if ($score > 1) { + $templateScore[$otherTemplate->slug] = $score; + } + } + }); + arsort($templateScore); // Sort by Score + + return array_slice(array_keys($templateScore), 0, self::MAX_RELATED_TEMPLATES); + } + + private function createFormTemplate( + array $formData, + string $formTitle, + string $formDescription, + string $formShortDescription, + array $formQAs, + ?string $imageUrl, + array $industry, + array $types, + array $relatedTemplates + ) { + return Template::create([ + 'name' => $formTitle, + 'description' => $formDescription, + 'short_description' => $formShortDescription, + 'questions' => $formQAs, + 'structure' => $formData, + 'image_url' => $imageUrl, + 'publicly_listed' => true, + 'industries' => $industry, + 'types' => $types, + 'related_templates' => $relatedTemplates, + ]); + } + + private function setReverseRelatedTemplates(Template $newTemplate) + { + if (!$newTemplate || count($newTemplate->related_templates) === 0) { + return; + } + + $templates = Template::whereIn('slug', $newTemplate->related_templates)->get(); + foreach ($templates as $template) { + if (count($template->related_templates) < self::MAX_RELATED_TEMPLATES) { + $template->update(['related_templates' => array_merge($template->related_templates, [$newTemplate->slug])]); + } + } + } +} diff --git a/api/routes/api.php b/api/routes/api.php index 6a362df7..39d61d6a 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -225,6 +225,10 @@ Route::group(['middleware' => 'auth:api'], function () { }); Route::group(['middleware' => 'moderator', 'prefix' => 'moderator'], function () { + Route::post( + 'create-template', + [\App\Http\Controllers\Admin\AdminController::class, 'createTemplate'] + ); Route::get( 'fetch-user/{identifier}', [\App\Http\Controllers\Admin\AdminController::class, 'fetchUser'] diff --git a/client/pages/settings/admin.vue b/client/pages/settings/admin.vue index 80e35682..df7d9f3a 100644 --- a/client/pages/settings/admin.vue +++ b/client/pages/settings/admin.vue @@ -62,6 +62,28 @@ Fetch User + +
+ + + Create Template + +
w.plan === 'pro')) { this.userPlan = 'pro' } + }, + async createTemplate() { + if (!this.createTemplateForm.template_prompt) { + this.useAlert.error('Template prompt is required.') + return + } + + this.templateLoading = true + opnFetch(`/moderator/create-template`, { + method: 'POST', + body: { + template_prompt: this.createTemplateForm.template_prompt + } + }).then((data) => { + this.templateLoading = false + this.createTemplateForm.reset() + this.useAlert.success('Template created.') + useRouter().push({ name: 'templates-slug', params: { slug: data.template_slug } }) + }) + .catch((error) => { + this.templateLoading = false + this.useAlert.error(error.data.message) + }) } } }