fix(ai): switch to OpenAI, add error handling in Insights

- AiService: use OpenAI chat completions API (gpt-4o-mini) instead of Anthropic
- Add services.openai.api_key config entry
- Insights component: check for API key, handle empty links, catch HTTP errors
- View: show error banner, hide placeholder when error shown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-17 14:40:11 +02:00
parent 3eada67b9a
commit d9f665f749
4 changed files with 54 additions and 23 deletions

View File

@ -6,24 +6,23 @@ use Illuminate\Support\Facades\Http;
class AiService
{
public function complete(string $prompt, string $model = 'claude-sonnet-4-6'): string
public function complete(string $prompt, string $model = 'gpt-4o-mini'): string
{
$apiKey = config('services.anthropic.api_key');
$apiKey = config('services.openai.api_key');
if (! $apiKey) {
return '';
}
$response = Http::withHeaders([
'x-api-key' => $apiKey,
'anthropic-version' => '2023-06-01',
'content-type' => 'application/json',
])->post('https://api.anthropic.com/v1/messages', [
'model' => $model,
'max_tokens' => 256,
'messages' => [['role' => 'user', 'content' => $prompt]],
'Authorization' => 'Bearer ' . $apiKey,
'Content-Type' => 'application/json',
])->post('https://api.openai.com/v1/chat/completions', [
'model' => $model,
'max_tokens' => 512,
'messages' => [['role' => 'user', 'content' => $prompt]],
]);
return trim($response->json('content.0.text') ?? '');
return trim($response->json('choices.0.message.content') ?? '');
}
}

View File

@ -12,7 +12,7 @@ class Insights extends Component
{
public string $insights = '';
public bool $loading = false;
public string $error = '';
public int $workspaceId = 0;
@ -23,21 +23,43 @@ class Insights extends Component
public function generate(): void
{
$workspace = Workspace::findOrFail($this->workspaceId);
$this->loading = true;
$this->error = '';
$this->insights = '';
$topLinks = Link::where('workspace_id', $workspace->id)
->withCount('clicks')
->orderByDesc('clicks_count')
->limit(10)
->get(['slug', 'target_url', 'clicks_count']);
if (! config('services.openai.api_key')) {
$this->error = 'OPENAI_API_KEY is not configured. Add it to .env and restart.';
return;
}
$summary = $topLinks->map(fn ($l) => "{$l->slug}: {$l->clicks_count} clicks → {$l->target_url}")->implode("\n");
try {
$workspace = Workspace::findOrFail($this->workspaceId);
$prompt = "You are a marketing analyst. Analyze these link performance stats and provide 3-5 actionable insights:\n\n{$summary}\n\nBe concise and specific.";
$topLinks = Link::where('workspace_id', $workspace->id)
->withCount('clicks')
->orderByDesc('clicks_count')
->limit(10)
->get(['slug', 'target_url', 'clicks_count']);
$this->insights = app(AiService::class)->complete($prompt);
$this->loading = false;
if ($topLinks->isEmpty()) {
$this->error = 'No links with clicks found. Create and share some links first.';
return;
}
$summary = $topLinks->map(fn ($l) => "{$l->slug}: {$l->clicks_count} clicks → {$l->target_url}")->implode("\n");
$prompt = "You are a marketing analyst. Analyze these link performance stats and provide 3-5 actionable insights:\n\n{$summary}\n\nBe concise and specific.";
$result = app(AiService::class)->complete($prompt);
if ($result === '') {
$this->error = 'AI returned an empty response. Check your API key and try again.';
return;
}
$this->insights = $result;
} catch (\Throwable $e) {
$this->error = 'Failed to generate insights: ' . $e->getMessage();
}
}
public function render(): View

View File

@ -39,4 +39,8 @@ return [
'api_key' => env('ANTHROPIC_API_KEY'),
],
'openai' => [
'api_key' => env('OPENAI_API_KEY'),
],
];

View File

@ -10,6 +10,12 @@
</x-ui.button>
</div>
@if($error)
<div class="p-4 bg-red-500/10 border border-red-500/20 rounded-xl text-sm text-red-400">
{{ $error }}
</div>
@endif
@if($insights)
<div class="p-5 bg-s1 border border-white/[.06] rounded-xl">
<div class="flex items-center gap-2 mb-3 text-accent text-xs font-medium">
@ -18,7 +24,7 @@
</div>
<div class="text-sm text-t1 whitespace-pre-wrap leading-relaxed">{{ $insights }}</div>
</div>
@else
@elseif(!$error)
<div class="p-8 bg-s1 border border-white/[.06] rounded-xl text-center">
<x-heroicon-o-sparkles class="w-10 h-10 mx-auto text-t3 mb-3" />
<p class="text-sm text-t2">Click "Generate Insights" to analyze your top 10 links with AI.</p>