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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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'),
|
||||
|
||||
@@ -243,5 +243,8 @@ document.addEventListener('alpine:init', () => {
|
||||
|
||||
@include('partials.sso-keepalive')
|
||||
@include('partials.confirm-prompt')
|
||||
@auth
|
||||
@include('partials.afia')
|
||||
@endauth
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user