From 6bb181bbc18bcdf3259038463a19a4d220c304c4 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Mon, 29 Jun 2026 20:47:42 +0000 Subject: [PATCH] Fix onboarding 500 by replacing leftover Frontdesk route references. Point layout partials at qms routes and add wallet/afia endpoints so shared shell renders on queue.ladill.com. Co-authored-by: Cursor --- app/Http/Controllers/Qms/AiController.php | 63 +++++++ app/Services/Afia/AfiaService.php | 177 ++++++++++++++++++ config/afia.php | 11 ++ .../views/components/app-layout.blade.php | 8 +- resources/views/partials/afia.blade.php | 14 +- resources/views/partials/topbar.blade.php | 6 +- .../partials/wallet-topup-modal.blade.php | 2 +- routes/web.php | 3 + 8 files changed, 269 insertions(+), 15 deletions(-) create mode 100644 app/Http/Controllers/Qms/AiController.php create mode 100644 app/Services/Afia/AfiaService.php create mode 100644 config/afia.php diff --git a/app/Http/Controllers/Qms/AiController.php b/app/Http/Controllers/Qms/AiController.php new file mode 100644 index 0000000..e62f3d1 --- /dev/null +++ b/app/Http/Controllers/Qms/AiController.php @@ -0,0 +1,63 @@ +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($request)); + } 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 */ + private function context(Request $request): array + { + $organization = app(OrganizationResolver::class)->resolveForUser($request->user()); + + if ($organization === null) { + return ['signed_in' => 'yes', 'setup' => 'onboarding_pending']; + } + + $owner = $this->ownerRef($request); + $ticketQuery = Ticket::owned($owner)->where('organization_id', $organization->id); + + return [ + 'signed_in' => 'yes', + 'organization' => $organization->name, + 'active_queues' => ServiceQueue::owned($owner)->where('organization_id', $organization->id)->where('is_active', true)->count(), + 'tickets_waiting' => (clone $ticketQuery)->where('status', 'waiting')->count(), + 'tickets_serving' => (clone $ticketQuery)->whereIn('status', ['called', 'serving'])->count(), + 'tickets_completed_today' => (clone $ticketQuery)->where('status', 'completed')->whereDate('completed_at', today())->count(), + ]; + } +} diff --git a/app/Services/Afia/AfiaService.php b/app/Services/Afia/AfiaService.php new file mode 100644 index 0000000..ff91844 --- /dev/null +++ b/app/Services/Afia/AfiaService.php @@ -0,0 +1,177 @@ +hasLocalKey() || $this->hasPlatformRelay(); + } + + /** + * @param array $history + * @param array $context + */ + public function chat(string $message, array $history, array $context): string + { + if (! $this->enabled()) { + throw new RuntimeException('Afia is not configured.'); + } + + if ($this->hasLocalKey()) { + return $this->chatLocally($message, $history, $context); + } + + return $this->chatViaPlatform($message, $history, $context); + } + + /** + * @param array $history + * @param array $context + */ + private function chatLocally(string $message, array $history, array $context): string + { + $provider = (string) config('afia.provider', 'openai'); + $model = (string) config('afia.model', 'gpt-4o-mini'); + $apiKey = (string) config('afia.api_key'); + + $messages = [['role' => 'system', 'content' => $this->systemPrompt($context)]]; + foreach (array_slice($history, -8) as $turn) { + $role = ($turn['role'] ?? 'user') === 'assistant' ? 'assistant' : 'user'; + $text = trim((string) ($turn['text'] ?? '')); + if ($text !== '') { + $messages[] = ['role' => $role, 'content' => $text]; + } + } + $messages[] = ['role' => 'user', 'content' => $message]; + + return $provider === 'anthropic' + ? $this->viaAnthropic($model, $apiKey, $messages) + : $this->viaOpenAi($model, $apiKey, $messages); + } + + /** + * @param array $history + * @param array $context + */ + private function chatViaPlatform(string $message, array $history, array $context): string + { + $base = rtrim((string) config('afia.platform_api_url', ''), '/'); + $token = (string) config('afia.platform_api_key', ''); + + $res = Http::withToken($token)->acceptJson()->timeout(50)->post($base.'/afia/chat', [ + 'product' => (string) config('afia.product', 'queue'), + 'message' => $message, + 'history' => $history, + 'system_prompt' => $this->systemPrompt($context), + ]); + + if ($res->status() === 503) { + throw new RuntimeException('Afia is not configured on the platform.'); + } + + if ($res->failed()) { + throw new RuntimeException('Platform Afia relay failed: '.$res->status()); + } + + $reply = trim((string) $res->json('reply', '')); + if ($reply === '') { + throw new RuntimeException('Platform Afia relay returned an empty response.'); + } + + return $reply; + } + + private function hasLocalKey(): bool + { + return (string) config('afia.api_key', '') !== ''; + } + + private function hasPlatformRelay(): bool + { + return rtrim((string) config('afia.platform_api_url', ''), '/') !== '' + && (string) config('afia.platform_api_key', '') !== ''; + } + + private function viaOpenAi(string $model, string $apiKey, array $messages): string + { + $res = Http::withToken($apiKey)->acceptJson()->timeout(45) + ->post('https://api.openai.com/v1/chat/completions', [ + 'model' => $model, + 'temperature' => 0.3, + 'max_tokens' => 600, + 'messages' => $messages, + ]); + + if ($res->failed()) { + throw new RuntimeException('OpenAI request failed: '.$res->status()); + } + + return trim((string) $res->json('choices.0.message.content', '')); + } + + private function viaAnthropic(string $model, string $apiKey, array $messages): string + { + $system = $messages[0]['content'] ?? ''; + $turns = array_values(array_filter($messages, fn ($m) => $m['role'] !== 'system')); + + $res = Http::withHeaders([ + 'x-api-key' => $apiKey, + 'anthropic-version' => '2023-06-01', + ])->acceptJson()->timeout(45)->post('https://api.anthropic.com/v1/messages', [ + 'model' => $model, + 'max_tokens' => 600, + 'system' => $system, + 'messages' => array_map(fn ($m) => ['role' => $m['role'], 'content' => $m['content']], $turns), + ]); + + if ($res->failed()) { + throw new RuntimeException('Anthropic request failed: '.$res->status()); + } + + return trim((string) $res->json('content.0.text', '')); + } + + /** @param array $context */ + private function systemPrompt(array $context): string + { + $ctx = collect($context)->map(fn ($v, $k) => "- {$k}: {$v}")->implode("\n"); + + return << env('AFIA_PRODUCT', 'queue'), + 'enabled' => (bool) env('AFIA_ENABLED', true), + 'provider' => env('AFIA_PROVIDER', 'openai'), + 'model' => env('AFIA_MODEL', 'gpt-4o-mini'), + 'api_key' => env('AFIA_API_KEY', env('OPENAI_API_KEY')), + 'platform_api_url' => env('AFIA_PLATFORM_API_URL', env('IDENTITY_API_URL', 'https://ladill.com/api')), + 'platform_api_key' => env('AFIA_PLATFORM_API_KEY', env('IDENTITY_API_KEY_QUEUE')), +]; diff --git a/resources/views/components/app-layout.blade.php b/resources/views/components/app-layout.blade.php index 52887ea..4498c15 100644 --- a/resources/views/components/app-layout.blade.php +++ b/resources/views/components/app-layout.blade.php @@ -40,10 +40,10 @@ : ($navUser?->avatar_url ?? null); @endphp @include('partials.mobile-bottom-nav', [ - 'homeUrl' => route('frontdesk.dashboard'), - 'homeActive' => request()->routeIs('frontdesk.dashboard'), - 'searchUrl' => route('frontdesk.visitors.index'), - 'searchActive' => request()->routeIs('frontdesk.visitors.*'), + 'homeUrl' => route('qms.dashboard'), + 'homeActive' => request()->routeIs('qms.dashboard'), + 'searchUrl' => route('qms.tickets.index'), + 'searchActive' => request()->routeIs('qms.tickets.*'), 'notificationsUrl' => route('notifications.index'), 'notificationsActive' => request()->routeIs('notifications.*'), 'unreadUrl' => route('notifications.unread'), diff --git a/resources/views/partials/afia.blade.php b/resources/views/partials/afia.blade.php index 265fdb5..7471d4a 100644 --- a/resources/views/partials/afia.blade.php +++ b/resources/views/partials/afia.blade.php @@ -1,15 +1,15 @@ @php - $afiaGreeting = "Hi, I'm Afia 👋 Ask me about visitor check-in, kiosks, hosts, badges, devices, or setting up your reception desk…"; + $afiaGreeting = "Hi, I'm Afia 👋 Ask me about queues, tickets, counters, displays, appointments, or setting up your branches…"; $afiaSuggestions = [ - 'How do I set up a visitor kiosk?', - 'How do I check a visitor in?', - 'Where do I add reception desks?', - 'How do hosts approve visits?', + 'How do I create a service queue?', + 'How do I issue tickets from the kiosk?', + 'Where do I set up counters and consoles?', + 'How do digital displays work?', ]; @endphp {{-- Afia — Ladill AI assistant slide-over. Opened via $dispatch('afia-open'). --}}

Afia

-

Frontdesk assistant

+

Queue assistant

diff --git a/resources/views/partials/topbar.blade.php b/resources/views/partials/topbar.blade.php index d339bb5..5e04c46 100644 --- a/resources/views/partials/topbar.blade.php +++ b/resources/views/partials/topbar.blade.php @@ -12,14 +12,14 @@ {{-- Mobile: app name --}} @include('partials.mobile-topbar-title') - {{-- Desktop: search (visitors) --}} - diff --git a/resources/views/partials/wallet-topup-modal.blade.php b/resources/views/partials/wallet-topup-modal.blade.php index d3e6aa3..addb9f5 100644 --- a/resources/views/partials/wallet-topup-modal.blade.php +++ b/resources/views/partials/wallet-topup-modal.blade.php @@ -6,7 +6,7 @@

Add funds to your Ladill wallet to continue.

-
+ @csrf diff --git a/routes/web.php b/routes/web.php index 2a490f2..99ddf2a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,6 +2,7 @@ use App\Http\Controllers\Auth\SsoLoginController; use App\Http\Controllers\NotificationController; +use App\Http\Controllers\Qms\AiController; use App\Http\Controllers\Qms\AnalyticsController; use App\Http\Controllers\Qms\AppointmentController; use App\Http\Controllers\Qms\AuditLogController; @@ -72,6 +73,8 @@ Route::middleware(['auth', 'platform.session'])->group(function () { Route::get('/onboarding', [OnboardingController::class, 'show'])->name('qms.onboarding.show'); Route::post('/onboarding', [OnboardingController::class, 'store'])->name('qms.onboarding.store'); + Route::post('/ai/chat', [AiController::class, 'chat'])->middleware('throttle:30,1')->name('qms.ai.chat'); + Route::get('/wallet', fn () => redirect()->away(ladill_account_url('/wallet')))->name('qms.wallet'); Route::middleware(['qms.setup'])->group(function () { Route::get('/dashboard', [DashboardController::class, 'index'])->name('qms.dashboard');