Fix Afia AI assistant for Ladill Link.
Deploy Ladill Link / deploy (push) Successful in 31s

Wire the chat route and Link-scoped system prompt, include the Afia panel in the main layout, and point the UI at link.afia.chat instead of the copied QR Plus route.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-27 14:31:00 +00:00
co-authored by Cursor
parent f1fe749934
commit 5a3631cd2e
8 changed files with 127 additions and 37 deletions
+6
View File
@@ -32,4 +32,10 @@ IDENTITY_API_KEY_LINK=
LINK_PRICE_PER_LINK_GHS=0.05
AFIA_ENABLED=true
AFIA_PRODUCT=link
AFIA_PROVIDER=openai
AFIA_MODEL=gpt-4o-mini
AFIA_API_KEY=
VITE_APP_NAME="${APP_NAME}"
@@ -0,0 +1,84 @@
<?php
namespace App\Http\Controllers\Link;
use App\Http\Controllers\Controller;
use App\Models\LinkWallet;
use App\Models\ShortLink;
use App\Services\Afia\AfiaService;
use App\Services\Billing\BillingClient;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class AfiaController extends Controller
{
public function chat(Request $request, AfiaService $afia): JsonResponse
{
$validated = $request->validate([
'message' => ['required', 'string', 'max:2000'],
'history' => ['nullable', 'array', 'max:20'],
'history.*.role' => ['nullable', 'string'],
'history.*.text' => ['nullable', 'string'],
]);
if (! $afia->enabled()) {
return response()->json(['message' => 'Afia is not available right now.'], 503);
}
try {
$reply = $afia->chat(trim($validated['message']), $validated['history'] ?? [], $this->context());
} catch (\Throwable $e) {
report($e);
return response()->json(['message' => 'Afia could not respond right now. Please try again.'], 502);
}
return response()->json(['reply' => $reply]);
}
/** @return array<string,mixed> */
private function context(): array
{
$account = ladill_account();
if (! $account) {
return ['signed_in' => 'no'];
}
$links = $account->shortLinks();
$ctx = [
'signed_in' => 'yes',
'links_total' => (clone $links)->count(),
'links_active' => (clone $links)->where('is_active', true)->count(),
'clicks_total' => (int) (clone $links)->sum('clicks_total'),
'custom_domains' => $account->linkCustomDomains()->count(),
'price_per_link_ghs' => number_format(LinkWallet::pricePerLink(), 2),
'default_public_domain' => (string) config('link.public_domain', 'ladl.link'),
];
if ($account->public_id) {
try {
$ctx['wallet_balance_ghs'] = number_format(app(BillingClient::class)->balanceMinor((string) $account->public_id) / 100, 2);
} catch (\Throwable) {
}
}
$recent = $account->shortLinks()
->latest('updated_at')
->limit(3)
->get(['slug', 'label', 'clicks_total', 'source_app', 'is_managed_here']);
if ($recent->isNotEmpty()) {
$ctx['recent_links'] = $recent->map(fn (ShortLink $link): string => sprintf(
'%s → %s (%d clicks%s)',
$link->label ?: $link->slug,
$link->publicUrl($account),
$link->clicks_total,
$link->is_managed_here ? '' : ', managed in '.$link->sourceAppLabel(),
))->implode('; ');
}
return $ctx;
}
}
+18 -24
View File
@@ -6,7 +6,7 @@ use Illuminate\Support\Facades\Http;
use RuntimeException;
/**
* Afia Ladill in-app AI assistant scoped to a product (QR Plus).
* Afia Ladill in-app AI assistant scoped to Ladill Link.
*/
class AfiaService
{
@@ -88,36 +88,30 @@ class AfiaService
{
$ctx = collect($context)->map(fn ($v, $k) => "- {$k}: {$v}")->implode("\n");
return match ((string) config('afia.product', 'qr')) {
'qr' => $this->qrSystemPrompt($ctx),
default => $this->qrSystemPrompt($ctx),
};
return $this->linkSystemPrompt($ctx);
}
private function qrSystemPrompt(string $ctx): string
private function linkSystemPrompt(string $ctx): string
{
return <<<PROMPT
You are Afia, the assistant inside Ladill QR Plus (qrplus.ladill.com).
Help users create and manage dynamic QR codes, understand scans and billing, and print codes that work reliably.
Be concise, friendly, and actionable: short numbered steps and name the screen or section to use.
You are Afia, the assistant inside Ladill Link (link.ladill.com).
Help users create and manage short links, track clicks, use custom domains, and understand wallet billing. Be concise, friendly, and actionable: short numbered steps and name the screen or section to use.
What Ladill QR Plus does and where things live:
- Overview (Dashboard): recent codes, active code count, scans in the last 30 days, wallet balance.
- My Codes: list, create, and edit QR codes. Types: Link (URL), PDF, List of Links, Business profile, WiFi, App download.
- Creating a code: pick a type, set content, customize style (colors, logo, frame), then download PNG/SVG/PDF.
- Dynamic codes: destination can change after printing the printed QR always points to ladill.com/q/{code}.
- Business QR: name, tagline, contact, hours, logo, cover, social links shown as a mobile landing page.
- WiFi QR: guests scan to join; network name and password are encoded in the code.
- PDF QR: hosts a PDF with optional download button on the viewer.
- Billing: each new code debits the Ladill wallet (account portal Wallet). Top up at account.ladill.com/wallet.
- Team: invite teammates to manage codes together (sidebar Team).
- Developers: API tokens for programmatic code creation (sidebar Developers).
What Ladill Link does and where things live:
- Overview (Dashboard): link count, recent clicks, and wallet balance.
- My Links: create, edit, enable/disable, and delete short links. Each new link debits a small fee from the Ladill wallet (see Settings or the create screen for the current price).
- Public URLs: default domain is ladl.link/{slug}. Links can also use a verified custom domain from Custom Domains.
- Analytics: click totals and trends across links.
- Custom Domains: connect a domain you own, verify DNS, and set a default domain for new links.
- Settings: default domain preference and account shortcuts.
- Unified catalog: links created in other Ladill apps (Merchant, Give, Events, QR Plus, etc.) also appear in My Links some are managed in their home app, others here.
- Wallet: link creation is paid from the Ladill wallet; top up at account.ladill.com/wallet.
Rules:
- Only answer questions about Ladill QR Plus code types, styling, downloads, scans, wallet billing, team, and API.
- If asked about domains, hosting, email, or payments/commerce QR (shop, events, donations), briefly say those live in other Ladill apps and suggest the app launcher.
- Never invent prices, short codes, or wallet balances use the user context below or tell them where to check.
- For print tips: recommend high contrast, adequate quiet zone, test-scan before mass printing.
- Only answer questions about Ladill Link short links, slugs, destinations, clicks, custom domains, billing, and the unified link list.
- If asked about QR code design, donations, events, or storefronts, briefly say those live in other Ladill apps and suggest the app launcher.
- Never invent slugs, click counts, or wallet balances use the user context below or tell them where to check.
- For DNS/custom domain help: mention verifying the TXT/CNAME record in Custom Domains before the domain goes live.
Current user context:
{$ctx}
+2 -2
View File
@@ -1,8 +1,8 @@
<?php
return [
// Afia — in-app AI assistant scoped to Ladill QR Plus.
'product' => env('AFIA_PRODUCT', 'qr'),
// Afia — in-app AI assistant scoped to Ladill Link.
'product' => env('AFIA_PRODUCT', 'link'),
'enabled' => (bool) env('AFIA_ENABLED', true),
'provider' => env('AFIA_PROVIDER', 'openai'), // openai | anthropic
'model' => env('AFIA_MODEL', 'gpt-4o-mini'),
+3
View File
@@ -243,5 +243,8 @@ document.addEventListener('alpine:init', () => {
@include('partials.sso-keepalive')
@include('partials.confirm-prompt')
@auth
@include('partials.afia')
@endauth
</body>
</html>
+7 -7
View File
@@ -1,15 +1,15 @@
@php
$afiaGreeting = "Hi, I'm Afia 👋 Ask me about QR codes — creating a Business or WiFi code, styling and downloads, scan stats, or wallet billing…";
$afiaGreeting = "Hi, I'm Afia 👋 Ask me about short links — creating one, custom domains, click analytics, or wallet billing…";
$afiaSuggestions = [
'How do I create a Business QR code?',
'What is the difference between Link and List of Links?',
'How do I download a print-ready PNG?',
'Why is my wallet balance low?',
'How do I create a short link?',
'How do I connect a custom domain?',
'Where do I see click stats?',
'Why was my wallet debited when I created a link?',
];
@endphp
{{-- Afia Ladill AI assistant slide-over. Opened via $dispatch('afia-open'). --}}
<div x-data="afia({
chatUrl: '{{ route('qr.afia.chat') }}',
chatUrl: '{{ route('link.afia.chat') }}',
csrf: '{{ csrf_token() }}',
greeting: @js($afiaGreeting),
suggestions: @js($afiaSuggestions),
@@ -48,7 +48,7 @@
</span>
<div class="leading-tight">
<p class="text-sm font-semibold text-slate-900">Afia</p>
<p class="text-[11px] text-slate-400">QR Plus assistant</p>
<p class="text-[11px] text-slate-400">Link assistant</p>
</div>
</div>
<div class="flex items-center gap-1">
+3
View File
@@ -1,6 +1,7 @@
<?php
use App\Http\Controllers\Auth\SsoLoginController;
use App\Http\Controllers\Link\AfiaController;
use App\Http\Controllers\Link\AnalyticsController;
use App\Http\Controllers\Link\CustomDomainController;
use App\Http\Controllers\Link\LinkController;
@@ -50,6 +51,8 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
Route::patch('/links/{link}', [LinkController::class, 'update'])->name('user.links.update');
Route::delete('/links/{link}', [LinkController::class, 'destroy'])->name('user.links.destroy');
Route::post('/afia/chat', [AfiaController::class, 'chat'])->name('link.afia.chat');
Route::get('/wallet', fn () => redirect()->away(ladill_account_url('/wallet')))->name('account.wallet');
Route::get('/billing', fn () => redirect()->away(ladill_account_url('/billing')))->name('account.billing');
});
+4 -4
View File
@@ -39,16 +39,16 @@ class AfiaTest extends TestCase
{
config(['afia.api_key' => 'sk-test', 'afia.provider' => 'openai', 'afia.model' => 'gpt-4o-mini']);
Http::fake([
'api.openai.com/*' => Http::response(['choices' => [['message' => ['content' => 'Create a Business QR under My Codes.']]]]),
'api.openai.com/*' => Http::response(['choices' => [['message' => ['content' => 'Create a short link under My Links.']]]]),
]);
$this->actingAs($this->user())
->postJson('/afia/chat', ['message' => 'How do I create a business QR?'])
->postJson('/afia/chat', ['message' => 'How do I create a short link?'])
->assertOk()
->assertJson(['reply' => 'Create a Business QR under My Codes.']);
->assertJson(['reply' => 'Create a short link under My Links.']);
Http::assertSent(fn ($r) => str_contains($r->url(), 'openai.com')
&& collect($r['messages'])->first()['role'] === 'system'
&& str_contains(collect($r['messages'])->first()['content'], 'Ladill QR Plus'));
&& str_contains(collect($r['messages'])->first()['content'], 'Ladill Link'));
}
}