From dac4f25a98148a24eec3d2e6f405dc08add07639 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 19:48:30 +0000 Subject: [PATCH] feat: implement AI auto-generated descriptions for themes using OpenRouter - Configure OpenRouter in config/services.php - Create AiService for interacting with OpenRouter API - Update ThemeObserver to generate description from name and colors on creation - Add OPENROUTER_API_KEY and OPENROUTER_MODEL to .env.example - Add feature test for AI description generation Co-authored-by: claudemyburgh <6057076+claudemyburgh@users.noreply.github.com> --- .env.example | 3 ++ app/Observers/ThemeObserver.php | 9 +++++ app/Services/AiService.php | 45 +++++++++++++++++++++++++ config/services.php | 5 +++ tests/Feature/AiDescriptionTest.php | 51 +++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 app/Services/AiService.php create mode 100644 tests/Feature/AiDescriptionTest.php diff --git a/.env.example b/.env.example index ae0e0c3..654deba 100644 --- a/.env.example +++ b/.env.example @@ -75,3 +75,6 @@ PADDLE_PRICE_PRO_YEARLY= VITE_PADDLE_PRICE_PRO_MONTHLY="${PADDLE_PRICE_PRO_MONTHLY}" VITE_PADDLE_PRICE_PRO_YEARLY="${PADDLE_PRICE_PRO_YEARLY}" + +OPENROUTER_API_KEY= +OPENROUTER_MODEL=nvidia/nemotron-3-super-120b-a12b:free diff --git a/app/Observers/ThemeObserver.php b/app/Observers/ThemeObserver.php index 43fba32..cc60282 100644 --- a/app/Observers/ThemeObserver.php +++ b/app/Observers/ThemeObserver.php @@ -3,6 +3,7 @@ namespace App\Observers; use App\Models\Theme; +use App\Services\AiService; use Illuminate\Support\Str; class ThemeObserver @@ -12,5 +13,13 @@ public function creating(Theme $theme): void if (! $theme->title) { $theme->title = Str::headline($theme->name); } + + if (! $theme->description) { + $ai = app(AiService::class); + $theme->description = $ai->generateThemeDescription( + $theme->name, + $theme->vars_light ?? [] + ); + } } } diff --git a/app/Services/AiService.php b/app/Services/AiService.php new file mode 100644 index 0000000..7d5cb16 --- /dev/null +++ b/app/Services/AiService.php @@ -0,0 +1,45 @@ +map(fn ($value, $key) => "{$key}: {$value}") + ->implode(', '); + + $prompt = "Generate a short, engaging description (max 2 sentences) for a UI theme named \"{$name}\" that uses these colors: {$colorList}. The description should highlight the mood or style of the theme."; + + $response = Http::withHeaders([ + 'Authorization' => 'Bearer '.$apiKey, + 'HTTP-Referer' => config('app.url'), + 'X-Title' => config('app.name'), + ])->post('https://openrouter.ai/api/v1/chat/completions', [ + 'model' => config('services.openrouter.model'), + 'messages' => [ + [ + 'role' => 'user', + 'content' => $prompt, + ], + ], + ]); + + if ($response->failed()) { + return null; + } + + $data = $response->json(); + + return $data['choices'][0]['message']['content'] ?? null; + } +} diff --git a/config/services.php b/config/services.php index b8ced15..5c87866 100644 --- a/config/services.php +++ b/config/services.php @@ -46,4 +46,9 @@ 'redirect' => env('GOOGLE_REDIRECT_URI'), ], + 'openrouter' => [ + 'key' => env('OPENROUTER_API_KEY'), + 'model' => env('OPENROUTER_MODEL', 'nvidia/nemotron-3-super-120b-a12b:free'), + ], + ]; diff --git a/tests/Feature/AiDescriptionTest.php b/tests/Feature/AiDescriptionTest.php new file mode 100644 index 0000000..5615d9c --- /dev/null +++ b/tests/Feature/AiDescriptionTest.php @@ -0,0 +1,51 @@ + Http::response([ + 'choices' => [ + [ + 'message' => [ + 'content' => 'A beautiful dark theme with neon accents.' + ] + ] + ] + ], 200), + 'https://example.com/theme.json' => Http::response([ + 'name' => 'neon-dark', + 'cssVars' => [ + 'light' => [ + 'background' => '0 0% 100%', + 'foreground' => '222.2 84% 4.9%', + ], + ], + ], 200), + ]); + + config(['services.openrouter.key' => 'test-key']); + + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post(route('themes.store'), [ + 'url' => 'https://example.com/theme.json', + ]); + + $this->assertDatabaseHas('themes', [ + 'name' => 'neon-dark', + 'description' => 'A beautiful dark theme with neon accents.', + ]); + } +}