Skip to content

Commit 94607de

Browse files
Merge pull request #13 from designbycode/feat-ai-theme-description-14487603099871641718
feat: implement AI auto-generated descriptions for themes using OpenR…
2 parents 0629f6d + dac4f25 commit 94607de

5 files changed

Lines changed: 113 additions & 0 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,6 @@ PADDLE_PRICE_PRO_YEARLY=
7575

7676
VITE_PADDLE_PRICE_PRO_MONTHLY="${PADDLE_PRICE_PRO_MONTHLY}"
7777
VITE_PADDLE_PRICE_PRO_YEARLY="${PADDLE_PRICE_PRO_YEARLY}"
78+
79+
OPENROUTER_API_KEY=
80+
OPENROUTER_MODEL=nvidia/nemotron-3-super-120b-a12b:free

app/Observers/ThemeObserver.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Observers;
44

55
use App\Models\Theme;
6+
use App\Services\AiService;
67
use Illuminate\Support\Str;
78

89
class ThemeObserver
@@ -12,5 +13,13 @@ public function creating(Theme $theme): void
1213
if (! $theme->title) {
1314
$theme->title = Str::headline($theme->name);
1415
}
16+
17+
if (! $theme->description) {
18+
$ai = app(AiService::class);
19+
$theme->description = $ai->generateThemeDescription(
20+
$theme->name,
21+
$theme->vars_light ?? []
22+
);
23+
}
1524
}
1625
}

app/Services/AiService.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace App\Services;
4+
5+
use Illuminate\Support\Facades\Http;
6+
7+
class AiService
8+
{
9+
public function generateThemeDescription(string $name, array $colors): ?string
10+
{
11+
$apiKey = config('services.openrouter.key');
12+
13+
if (! $apiKey) {
14+
return null;
15+
}
16+
17+
$colorList = collect($colors)
18+
->map(fn ($value, $key) => "{$key}: {$value}")
19+
->implode(', ');
20+
21+
$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.";
22+
23+
$response = Http::withHeaders([
24+
'Authorization' => 'Bearer '.$apiKey,
25+
'HTTP-Referer' => config('app.url'),
26+
'X-Title' => config('app.name'),
27+
])->post('https://openrouter.ai/api/v1/chat/completions', [
28+
'model' => config('services.openrouter.model'),
29+
'messages' => [
30+
[
31+
'role' => 'user',
32+
'content' => $prompt,
33+
],
34+
],
35+
]);
36+
37+
if ($response->failed()) {
38+
return null;
39+
}
40+
41+
$data = $response->json();
42+
43+
return $data['choices'][0]['message']['content'] ?? null;
44+
}
45+
}

config/services.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,9 @@
4646
'redirect' => env('GOOGLE_REDIRECT_URI'),
4747
],
4848

49+
'openrouter' => [
50+
'key' => env('OPENROUTER_API_KEY'),
51+
'model' => env('OPENROUTER_MODEL', 'nvidia/nemotron-3-super-120b-a12b:free'),
52+
],
53+
4954
];
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use App\Models\User;
6+
use App\Services\AiService;
7+
use Illuminate\Foundation\Testing\RefreshDatabase;
8+
use Illuminate\Support\Facades\Http;
9+
use Tests\TestCase;
10+
11+
class AiDescriptionTest extends TestCase
12+
{
13+
use RefreshDatabase;
14+
15+
public function test_it_generates_description_on_theme_creation()
16+
{
17+
Http::fake([
18+
'https://openrouter.ai/*' => Http::response([
19+
'choices' => [
20+
[
21+
'message' => [
22+
'content' => 'A beautiful dark theme with neon accents.'
23+
]
24+
]
25+
]
26+
], 200),
27+
'https://example.com/theme.json' => Http::response([
28+
'name' => 'neon-dark',
29+
'cssVars' => [
30+
'light' => [
31+
'background' => '0 0% 100%',
32+
'foreground' => '222.2 84% 4.9%',
33+
],
34+
],
35+
], 200),
36+
]);
37+
38+
config(['services.openrouter.key' => 'test-key']);
39+
40+
$user = User::factory()->create();
41+
42+
$response = $this->actingAs($user)->post(route('themes.store'), [
43+
'url' => 'https://example.com/theme.json',
44+
]);
45+
46+
$this->assertDatabaseHas('themes', [
47+
'name' => 'neon-dark',
48+
'description' => 'A beautiful dark theme with neon accents.',
49+
]);
50+
}
51+
}

0 commit comments

Comments
 (0)