fix(ui): proper 3/4-arc spinner centered in button, honest AI prompt

- Button: wire:loading.remove on label (DOM removal, no z-index fight)
  spinner span is normal flex child — button's own flex centers it
  SVG: track circle opacity 25% + 3/4 arc via stroke-dasharray 42.4/56.6,
  stroke-linecap round, rotated -90deg so arc starts at top
- AI prompt: explicitly flags sparse data (<3 links or <10 clicks),
  requires every claim reference a specific slug, forbids generic padding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-17 15:05:31 +02:00
parent 8cd46398c0
commit add7957430
2 changed files with 15 additions and 15 deletions

View File

@ -54,21 +54,19 @@ class Insights extends Component
$summary = $topLinks->map(fn ($l) => "- {$l->slug} ({$l->clicks_count} clicks) → {$l->target_url}")->implode("\n"); $summary = $topLinks->map(fn ($l) => "- {$l->slug} ({$l->clicks_count} clicks) → {$l->target_url}")->implode("\n");
$prompt = <<<PROMPT $prompt = <<<PROMPT
You are a link performance analyst for a URL shortener platform. Respond in {$language}. You are a link performance analyst for a URL shortener. Respond in {$language}.
Workspace: {$workspace->name} Workspace: {$workspace->name}
Total links analysed: {$linkCount} | Total clicks: {$totalClicks} Links analysed: {$linkCount} | Total clicks: {$totalClicks}
Top links: Data:
{$summary} {$summary}
Provide 34 short, specific insights: Rules:
- Which link types or destinations perform best and why - If fewer than 3 links or fewer than 10 total clicks exist, say so upfront and explain that reliable patterns need more data. Give 12 observations possible from the limited data, then stop.
- Patterns across target domains (e.g. social, docs, pricing pages) - Otherwise give 34 insights: best-performing destination types, domain patterns, what underperforming links share, one concrete next action.
- What the low-click links have in common and how to improve them - Every claim must reference a specific slug or URL from the data above.
- One concrete next action for this workspace - No generic marketing advice. No padding. Plain language, no excessive formatting.
No generic marketing advice. Base every insight on the data above. Be direct, no fluff.
PROMPT; PROMPT;
$result = app(AiService::class)->complete($prompt); $result = app(AiService::class)->complete($prompt);

View File

@ -21,14 +21,16 @@ $cls = $base . ' ' . ($variants[$variant] ?? $variants['primary']);
{{ $attributes->merge(['type' => $type, 'class' => $cls]) }} {{ $attributes->merge(['type' => $type, 'class' => $cls]) }}
@if($action) wire:loading.attr="disabled" wire:target="{{ $action }}" @endif @if($action) wire:loading.attr="disabled" wire:target="{{ $action }}" @endif
> >
<span @if($action) wire:loading.class="opacity-40" wire:target="{{ $action }}" @endif class="flex items-center gap-2"> <span @if($action) wire:loading.remove wire:target="{{ $action }}" @endif class="flex items-center gap-2">
{{ $slot->isNotEmpty() ? $slot : $label }} {{ $slot->isNotEmpty() ? $slot : $label }}
</span> </span>
@if($action) @if($action)
<span wire:loading wire:target="{{ $action }}" class="absolute inset-0 flex items-center justify-center rounded-lg bg-inherit"> <span wire:loading wire:target="{{ $action }}" class="flex items-center justify-center">
<svg class="animate-spin h-5 w-5 flex-shrink-0" viewBox="0 0 24 24" fill="none"> <svg class="animate-spin w-5 h-5 flex-shrink-0" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/> <circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2.5" stroke-opacity="0.25"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/> <circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2.5"
stroke-dasharray="42.4 56.6" stroke-linecap="round"
transform="rotate(-90 12 12)"/>
</svg> </svg>
</span> </span>
@endif @endif