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
parent
3eada67b9a
commit
d9f665f749
|
|
@ -6,24 +6,23 @@ use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
class AiService
|
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) {
|
if (! $apiKey) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = Http::withHeaders([
|
$response = Http::withHeaders([
|
||||||
'x-api-key' => $apiKey,
|
'Authorization' => 'Bearer ' . $apiKey,
|
||||||
'anthropic-version' => '2023-06-01',
|
'Content-Type' => 'application/json',
|
||||||
'content-type' => 'application/json',
|
])->post('https://api.openai.com/v1/chat/completions', [
|
||||||
])->post('https://api.anthropic.com/v1/messages', [
|
'model' => $model,
|
||||||
'model' => $model,
|
'max_tokens' => 512,
|
||||||
'max_tokens' => 256,
|
'messages' => [['role' => 'user', 'content' => $prompt]],
|
||||||
'messages' => [['role' => 'user', 'content' => $prompt]],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return trim($response->json('content.0.text') ?? '');
|
return trim($response->json('choices.0.message.content') ?? '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ class Insights extends Component
|
||||||
{
|
{
|
||||||
public string $insights = '';
|
public string $insights = '';
|
||||||
|
|
||||||
public bool $loading = false;
|
public string $error = '';
|
||||||
|
|
||||||
public int $workspaceId = 0;
|
public int $workspaceId = 0;
|
||||||
|
|
||||||
|
|
@ -23,21 +23,43 @@ class Insights extends Component
|
||||||
|
|
||||||
public function generate(): void
|
public function generate(): void
|
||||||
{
|
{
|
||||||
$workspace = Workspace::findOrFail($this->workspaceId);
|
$this->error = '';
|
||||||
$this->loading = true;
|
$this->insights = '';
|
||||||
|
|
||||||
$topLinks = Link::where('workspace_id', $workspace->id)
|
if (! config('services.openai.api_key')) {
|
||||||
->withCount('clicks')
|
$this->error = 'OPENAI_API_KEY is not configured. Add it to .env and restart.';
|
||||||
->orderByDesc('clicks_count')
|
return;
|
||||||
->limit(10)
|
}
|
||||||
->get(['slug', 'target_url', 'clicks_count']);
|
|
||||||
|
|
||||||
$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);
|
if ($topLinks->isEmpty()) {
|
||||||
$this->loading = false;
|
$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
|
public function render(): View
|
||||||
|
|
|
||||||
|
|
@ -39,4 +39,8 @@ return [
|
||||||
'api_key' => env('ANTHROPIC_API_KEY'),
|
'api_key' => env('ANTHROPIC_API_KEY'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'openai' => [
|
||||||
|
'api_key' => env('OPENAI_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,12 @@
|
||||||
</x-ui.button>
|
</x-ui.button>
|
||||||
</div>
|
</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)
|
@if($insights)
|
||||||
<div class="p-5 bg-s1 border border-white/[.06] rounded-xl">
|
<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">
|
<div class="flex items-center gap-2 mb-3 text-accent text-xs font-medium">
|
||||||
|
|
@ -18,7 +24,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-t1 whitespace-pre-wrap leading-relaxed">{{ $insights }}</div>
|
<div class="text-sm text-t1 whitespace-pre-wrap leading-relaxed">{{ $insights }}</div>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@elseif(!$error)
|
||||||
<div class="p-8 bg-s1 border border-white/[.06] rounded-xl text-center">
|
<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" />
|
<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>
|
<p class="text-sm text-t2">Click "Generate Insights" to analyze your top 10 links with AI.</p>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue