diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php index 52fdb9e..862df83 100644 --- a/app/Http/Controllers/ChatController.php +++ b/app/Http/Controllers/ChatController.php @@ -23,4 +23,9 @@ class ChatController extends Controller { return view('chat.configuracion'); } + + public function iaLogs() + { + return view('chat.ia-logs'); + } } diff --git a/app/Http/Controllers/TelegramWebhookController.php b/app/Http/Controllers/TelegramWebhookController.php index e0b848d..3d7c7dd 100644 --- a/app/Http/Controllers/TelegramWebhookController.php +++ b/app/Http/Controllers/TelegramWebhookController.php @@ -31,13 +31,14 @@ class TelegramWebhookController extends Controller // Voice / audio messages if (isset($update['message']['voice']) || isset($update['message']['audio'])) { - $chatId = (string) $update['message']['chat']['id']; - $fileId = $update['message']['voice']['file_id'] ?? $update['message']['audio']['file_id']; - $nombre = trim( + $chatId = (string) $update['message']['chat']['id']; + $fileId = $update['message']['voice']['file_id'] ?? $update['message']['audio']['file_id']; + $duration = $update['message']['voice']['duration'] ?? $update['message']['audio']['duration'] ?? null; + $nombre = trim( ($update['message']['from']['first_name'] ?? '') . ' ' . ($update['message']['from']['last_name'] ?? '') ); - $bot->handleVoice($chatId, $fileId, $nombre); + $bot->handleVoice($chatId, $fileId, $nombre, (int) $duration); return response()->json(['ok' => true]); } diff --git a/app/Http/Livewire/Chat/ShowAiLogs.php b/app/Http/Livewire/Chat/ShowAiLogs.php new file mode 100644 index 0000000..17ab2dd --- /dev/null +++ b/app/Http/Livewire/Chat/ShowAiLogs.php @@ -0,0 +1,64 @@ +resetPage(); } + public function updatingFiltroCanal(): void { $this->resetPage(); } + public function updatingFiltroFechaDesde(): void { $this->resetPage(); } + public function updatingFiltroFechaHasta(): void { $this->resetPage(); } + + public function render() + { + $query = AiUsageLog::query()->latest(); + + if ($this->filtroServicio) { + $query->where('servicio', $this->filtroServicio); + } + if ($this->filtroCanal) { + $query->where('canal', $this->filtroCanal); + } + if ($this->filtroFechaDesde) { + $query->whereDate('created_at', '>=', $this->filtroFechaDesde); + } + if ($this->filtroFechaHasta) { + $query->whereDate('created_at', '<=', $this->filtroFechaHasta); + } + + $logs = $query->paginate(30); + + // Resumen total (aplicando mismos filtros sin paginación) + $resumenQuery = AiUsageLog::query(); + if ($this->filtroServicio) $resumenQuery->where('servicio', $this->filtroServicio); + if ($this->filtroCanal) $resumenQuery->where('canal', $this->filtroCanal); + if ($this->filtroFechaDesde) $resumenQuery->whereDate('created_at', '>=', $this->filtroFechaDesde); + if ($this->filtroFechaHasta) $resumenQuery->whereDate('created_at', '<=', $this->filtroFechaHasta); + + $resumen = $resumenQuery->selectRaw(' + COUNT(*) as total_llamadas, + SUM(CASE WHEN resultado = "ok" THEN 1 ELSE 0 END) as total_ok, + SUM(CASE WHEN resultado = "error" THEN 1 ELSE 0 END) as total_errores, + SUM(input_tokens) as total_input_tokens, + SUM(output_tokens) as total_output_tokens, + SUM(audio_segundos) as total_segundos_whisper, + SUM(CASE WHEN servicio = "whisper" THEN costo ELSE 0 END) as costo_whisper, + AVG(tiempo_ms) as avg_tiempo_ms + ')->first(); + + return view('livewire.chat.show-ai-logs', compact('logs', 'resumen')); + } +} diff --git a/app/Http/Livewire/Chat/ShowConfiguracionChat.php b/app/Http/Livewire/Chat/ShowConfiguracionChat.php old mode 100644 new mode 100755 index 0ceda8a..3443975 --- a/app/Http/Livewire/Chat/ShowConfiguracionChat.php +++ b/app/Http/Livewire/Chat/ShowConfiguracionChat.php @@ -16,6 +16,7 @@ class ShowConfiguracionChat extends Component public string $mensaje_bienvenida = ''; public string $mensaje_transferencia = ''; public string $webhookResult = ''; + public string $whisperResult = ''; // ── Gemini IA ───────────────────────────────────────────── public string $gemini_api_key = ''; @@ -85,6 +86,29 @@ class ShowConfiguracionChat extends Component $this->alert('success', 'Configuracion guardada correctamente.'); } + public function resetearWhisper(): void + { + $url = trim($this->whisper_url ?: ChatConfig::get('whisper_url')); + $token = trim($this->whisper_token ?: ChatConfig::get('whisper_token')); + + if (! $url) { + $this->whisperResult = '⚠️ La URL de Whisper está vacía.'; + return; + } + + try { + $response = Http::timeout(10) + ->withBasicAuth('whisper', $token) + ->get(rtrim($url, '/asr')); // ping al servidor + + $this->whisperResult = $response->successful() + ? '✅ Conexión con Whisper establecida correctamente.' + : '❌ Whisper respondió con error ' . $response->status() . '.'; + } catch (\Throwable $e) { + $this->whisperResult = '❌ No se pudo conectar con Whisper: ' . $e->getMessage(); + } + } + public function registrarWebhook(): void { $token = trim($this->telegram_token); diff --git a/app/Models/AiUsageLog.php b/app/Models/AiUsageLog.php new file mode 100644 index 0000000..62d175b --- /dev/null +++ b/app/Models/AiUsageLog.php @@ -0,0 +1,27 @@ + 'array']; + + public static function registrar(array $data): void + { + try { + static::create($data); + } catch (\Throwable) { + // No interrumpir el flujo principal si el log falla + } + } +} diff --git a/app/Services/GeminiIntentService.php b/app/Services/GeminiIntentService.php index 36dfa04..0a48a97 100644 --- a/app/Services/GeminiIntentService.php +++ b/app/Services/GeminiIntentService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Models\AiUsageLog; use App\Models\ChatConfig; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -20,13 +21,14 @@ class GeminiIntentService * Detecta la intención del texto del usuario y la mapea a una acción del sistema. * Retorna ['action' => string, 'data' => array] o null si no se puede detectar. */ - public function detectar(string $texto): ?array + public function detectar(string $texto, ?int $usuarioId = null, string $canal = 'web'): ?array { if (! $this->apiKey || ChatConfig::get('gemini_habilitado', '0') !== '1') { return null; } $prompt = $this->buildPrompt($texto); + $inicio = microtime(true); try { $response = Http::withHeaders(['Content-Type' => 'application/json']) @@ -39,16 +41,50 @@ class GeminiIntentService ], ]); + $tiempoMs = (int) ((microtime(true) - $inicio) * 1000); + if (! $response->successful()) { Log::warning('[Gemini Intent] API error: ' . $response->body()); + AiUsageLog::registrar([ + 'servicio' => 'gemini_intent', + 'canal' => $canal, + 'usuario_id'=> $usuarioId, + 'tiempo_ms' => $tiempoMs, + 'resultado' => 'error', + 'detalle' => ['error' => substr($response->body(), 0, 300), 'texto' => $texto], + ]); return null; } - $text = $response->json('candidates.0.content.parts.0.text', ''); - return $this->parseRespuesta($text); + $inputTokens = $response->json('usageMetadata.promptTokenCount', 0); + $outputTokens = $response->json('usageMetadata.candidatesTokenCount', 0); + $text = $response->json('candidates.0.content.parts.0.text', ''); + $resultado = $this->parseRespuesta($text); + + AiUsageLog::registrar([ + 'servicio' => 'gemini_intent', + 'canal' => $canal, + 'usuario_id' => $usuarioId, + 'input_tokens' => $inputTokens, + 'output_tokens'=> $outputTokens, + 'tiempo_ms' => $tiempoMs, + 'resultado' => 'ok', + 'detalle' => ['texto' => $texto, 'accion' => $resultado['action'] ?? null], + ]); + + return $resultado; } catch (\Throwable $e) { + $tiempoMs = (int) ((microtime(true) - $inicio) * 1000); Log::warning('[Gemini Intent] Excepcion: ' . $e->getMessage()); + AiUsageLog::registrar([ + 'servicio' => 'gemini_intent', + 'canal' => $canal, + 'usuario_id'=> $usuarioId, + 'tiempo_ms' => $tiempoMs, + 'resultado' => 'error', + 'detalle' => ['error' => $e->getMessage(), 'texto' => $texto], + ]); return null; } } diff --git a/app/Services/GeminiVisionService.php b/app/Services/GeminiVisionService.php index df6d7a5..fb504a0 100644 --- a/app/Services/GeminiVisionService.php +++ b/app/Services/GeminiVisionService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Models\AiUsageLog; use App\Models\ChatConfig; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -23,7 +24,7 @@ class GeminiVisionService * @param string $mimeType MIME type (image/jpeg, image/png, etc.) * @return array|null ['banco', 'valor', 'referencia', 'fecha', 'hora', 'remitente'] o null */ - public function extraerPago(string $base64, string $mimeType): ?array + public function extraerPago(string $base64, string $mimeType, ?int $usuarioId = null, string $canal = 'telegram'): ?array { if (! $this->apiKey) { Log::warning('[Gemini Vision] No hay API key configurada.'); @@ -31,6 +32,7 @@ class GeminiVisionService } $prompt = $this->buildPrompt(); + $inicio = microtime(true); try { $response = Http::withHeaders(['Content-Type' => 'application/json']) @@ -48,16 +50,50 @@ class GeminiVisionService ], ]); + $tiempoMs = (int) ((microtime(true) - $inicio) * 1000); + if (! $response->successful()) { Log::warning('[Gemini Vision] API error: ' . $response->body()); + AiUsageLog::registrar([ + 'servicio' => 'gemini_vision', + 'canal' => $canal, + 'usuario_id'=> $usuarioId, + 'tiempo_ms' => $tiempoMs, + 'resultado' => 'error', + 'detalle' => ['error' => substr($response->body(), 0, 300)], + ]); return null; } - $text = $response->json('candidates.0.content.parts.0.text', ''); - return $this->parseRespuesta($text); + $inputTokens = $response->json('usageMetadata.promptTokenCount', 0); + $outputTokens = $response->json('usageMetadata.candidatesTokenCount', 0); + $text = $response->json('candidates.0.content.parts.0.text', ''); + $resultado = $this->parseRespuesta($text); + + AiUsageLog::registrar([ + 'servicio' => 'gemini_vision', + 'canal' => $canal, + 'usuario_id' => $usuarioId, + 'input_tokens' => $inputTokens, + 'output_tokens'=> $outputTokens, + 'tiempo_ms' => $tiempoMs, + 'resultado' => $resultado ? 'ok' : 'error', + 'detalle' => $resultado ? ['banco' => $resultado['banco'], 'valor' => $resultado['valor']] : ['raw' => substr($text, 0, 200)], + ]); + + return $resultado; } catch (\Throwable $e) { + $tiempoMs = (int) ((microtime(true) - $inicio) * 1000); Log::error('[Gemini Vision] Excepcion: ' . $e->getMessage()); + AiUsageLog::registrar([ + 'servicio' => 'gemini_vision', + 'canal' => $canal, + 'usuario_id'=> $usuarioId, + 'tiempo_ms' => $tiempoMs, + 'resultado' => 'error', + 'detalle' => ['error' => $e->getMessage()], + ]); return null; } } diff --git a/app/Services/TelegramBotService.php b/app/Services/TelegramBotService.php index 20b356b..29afc5d 100644 --- a/app/Services/TelegramBotService.php +++ b/app/Services/TelegramBotService.php @@ -3,6 +3,7 @@ namespace App\Services; use App\Mail\ChatOtpMail; +use App\Models\AiUsageLog; use App\Models\ChatConfig; use App\Models\ChatContact; use App\Models\ChatConversation; @@ -85,6 +86,12 @@ class TelegramBotService $step = $state['step'] ?? 'await_email'; + // Guardar último texto para que handleFreeText pueda pasarlo a Gemini + if (! in_array($step, ['await_email', 'await_otp', 'await_nombre', 'await_celular', 'await_amount'])) { + $state['data']['last_text'] = $text; + $this->setState($chatId, $state); + } + match ($step) { 'await_email' => $this->processEmail($chatId, $text, $nombre), 'await_otp' => $this->processOtp($chatId, $text), @@ -161,7 +168,7 @@ class TelegramBotService $base64 = base64_encode($imageData['content']); $mime = $imageData['mime']; - $datosPago = app(GeminiVisionService::class)->extraerPago($base64, $mime); + $datosPago = app(GeminiVisionService::class)->extraerPago($base64, $mime, $state['user_id'] ?? null, 'telegram'); if (! $datosPago) { $this->send($chatId, "No pude leer el comprobante. Intenta con una foto más clara."); @@ -196,10 +203,11 @@ class TelegramBotService } } - public function handleVoice(string $chatId, string $fileId, string $nombre = ''): void + public function handleVoice(string $chatId, string $fileId, string $nombre = '', int $duracionSegundos = 0): void { $state = $this->getState($chatId); $this->convId = $state['conv_id'] ?? null; + $usuarioId = $state['user_id'] ?? null; if (ChatConfig::get('whisper_habilitado', '0') !== '1') { $this->send($chatId, "🎤 Recibí tu audio, pero la transcripción de voz no está habilitada. Por favor escribe tu mensaje."); @@ -215,15 +223,15 @@ class TelegramBotService return; } - $texto = $this->transcribeAudio($audioData['content']); - if (! $texto) { + $resultado = $this->transcribeAudio($audioData['content'], $duracionSegundos, $usuarioId); + + if (! $resultado['texto']) { $this->send($chatId, "No pude entender el audio. Intenta de nuevo o escribe tu mensaje."); return; } - // Confirmar lo que se escuchó y procesar como texto normal - $this->send($chatId, "🎙️ Escuché: _\"{$texto}\"_"); - $this->handleText($chatId, $texto, $nombre); + $this->send($chatId, "🎙️ Escuché: _\"{$resultado['texto']}\"_"); + $this->handleText($chatId, $resultado['texto'], $nombre); } catch (\Throwable $e) { Log::warning('[TelegramBot] handleVoice error: ' . $e->getMessage()); @@ -231,33 +239,62 @@ class TelegramBotService } } - private function transcribeAudio(string $audioContent): ?string + private function transcribeAudio(string $audioContent, int $duracionSegundos = 0, ?int $usuarioId = null): array { $whisperUrl = ChatConfig::get('whisper_url'); $whisperToken = ChatConfig::get('whisper_token'); + $inicio = microtime(true); if (! $whisperUrl) { - return null; + return ['texto' => null]; } try { - $response = Http::timeout(30) + $response = Http::timeout(60) ->withBasicAuth('whisper', $whisperToken) ->attach('audio_file', $audioContent, 'audio.ogg') ->post($whisperUrl, ['response_format' => 'json']); - if (! $response->successful()) { + $tiempoMs = (int) ((microtime(true) - $inicio) * 1000); + $json = $response->json(); + $texto = trim($json['text'] ?? $json['transcription'] ?? ''); + $ok = $response->successful() && $texto !== ''; + + AiUsageLog::registrar([ + 'servicio' => 'whisper', + 'canal' => 'telegram', + 'usuario_id' => $usuarioId, + 'audio_segundos' => $duracionSegundos ?: null, + 'tiempo_ms' => $tiempoMs, + 'costo' => $duracionSegundos, // $1 por segundo + 'resultado' => $ok ? 'ok' : 'error', + 'detalle' => [ + 'texto' => $ok ? substr($texto, 0, 200) : null, + 'error' => $ok ? null : substr($response->body(), 0, 300), + 'segundos' => $duracionSegundos, + ], + ]); + + if (! $ok) { Log::warning('[TelegramBot] Whisper error: ' . $response->body()); - return null; + return ['texto' => null]; } - // Whisper devuelve {"text": "..."} o {"transcription": "..."} - $json = $response->json(); - return trim($json['text'] ?? $json['transcription'] ?? ''); + return ['texto' => $texto]; } catch (\Throwable $e) { + $tiempoMs = (int) ((microtime(true) - $inicio) * 1000); Log::warning('[TelegramBot] Whisper exception: ' . $e->getMessage()); - return null; + AiUsageLog::registrar([ + 'servicio' => 'whisper', + 'canal' => 'telegram', + 'usuario_id' => $usuarioId, + 'audio_segundos' => $duracionSegundos ?: null, + 'tiempo_ms' => $tiempoMs, + 'resultado' => 'error', + 'detalle' => ['error' => $e->getMessage()], + ]); + return ['texto' => null]; } } @@ -1112,9 +1149,33 @@ class TelegramBotService } } + // Intentar detectar intención con Gemini antes de mostrar menú + $texto = $state['data']['last_text'] ?? ''; + if ($texto) { + $accion = app(GeminiIntentService::class)->detectar($texto, $state['user_id'] ?? null, 'telegram'); + if ($accion && $accion['action'] !== 'menu.principal') { + $this->dispatchAction($chatId, $accion['action'], $state); + return; + } + } + $this->showMainMenu($chatId); } + private function dispatchAction(string $chatId, string $action, array $state): void + { + match ($action) { + 'servicios.listar' => $this->listServices($chatId), + 'promo.listar' => $this->listPromos($chatId), + 'recarga.iniciar' => $this->showRechargeAmounts($chatId), + 'credenciales.listar' => $this->showCredentials($chatId), + 'historial.ver' => $this->showHistory($chatId), + 'perfil.ver' => $this->showProfile($chatId), + 'asesor.solicitar' => $this->transferToAgent($chatId), + default => $this->showMainMenu($chatId), + }; + } + // ─── State management ───────────────────────────────────── private function getState(string $chatId): array diff --git a/database/migrations/2026_07_14_000001_create_ai_usage_logs_table.php b/database/migrations/2026_07_14_000001_create_ai_usage_logs_table.php new file mode 100644 index 0000000..47538fe --- /dev/null +++ b/database/migrations/2026_07_14_000001_create_ai_usage_logs_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('servicio', 30); + $table->string('canal', 20)->default('telegram'); + $table->unsignedBigInteger('usuario_id')->nullable(); + $table->integer('input_tokens')->nullable(); + $table->integer('output_tokens')->nullable(); + $table->integer('audio_segundos')->nullable(); + $table->integer('tiempo_ms')->default(0); + $table->decimal('costo', 10, 2)->default(0); + $table->string('resultado', 10)->default('ok'); + $table->text('detalle')->nullable(); + $table->timestamps(); + + $table->index(['servicio', 'created_at']); + $table->index('usuario_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('ai_usage_logs'); + } +}; diff --git a/resources/views/chat/ia-logs.blade.php b/resources/views/chat/ia-logs.blade.php new file mode 100644 index 0000000..c5af7de --- /dev/null +++ b/resources/views/chat/ia-logs.blade.php @@ -0,0 +1,5 @@ +@extends('layouts.app') + +@section('content') + +@endsection diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index cd998d0..fa3da56 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -288,6 +288,10 @@ Correo / IMAP + + + Logs IA + @endif diff --git a/resources/views/livewire/chat/show-ai-logs.blade.php b/resources/views/livewire/chat/show-ai-logs.blade.php new file mode 100644 index 0000000..1cd7a4c --- /dev/null +++ b/resources/views/livewire/chat/show-ai-logs.blade.php @@ -0,0 +1,180 @@ +
+ +
+

Logs de uso de IA

+

Historial de llamadas a Gemini y Whisper con tokens, tiempos y costos.

+
+ + {{-- Cards resumen --}} +
+
+

Total llamadas

+

{{ number_format($resumen->total_llamadas ?? 0) }}

+

+ {{ number_format($resumen->total_ok ?? 0) }} ok +  ·  + {{ number_format($resumen->total_errores ?? 0) }} errores +

+
+
+

Tokens Gemini

+

{{ number_format(($resumen->total_input_tokens ?? 0) + ($resumen->total_output_tokens ?? 0)) }}

+

+ Entrada: {{ number_format($resumen->total_input_tokens ?? 0) }} · Salida: {{ number_format($resumen->total_output_tokens ?? 0) }} +

+
+
+

Audio Whisper

+

{{ number_format($resumen->total_segundos_whisper ?? 0) }}s

+

segundos transcritos

+
+
+

Costo Whisper

+

${{ number_format($resumen->costo_whisper ?? 0) }}

+

$1 por segundo · Tiempo prom: {{ number_format($resumen->avg_tiempo_ms ?? 0) }}ms

+
+
+ + {{-- Filtros --}} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + {{-- Tabla --}} +
+
+ + + + + + + + + + + + + + + + + @forelse($logs as $log) + @php + $detalle = is_array($log->detalle) ? $log->detalle : []; + $badgeServicio = match($log->servicio) { + 'gemini_intent' => 'bg-blue-100 text-blue-700', + 'gemini_vision' => 'bg-indigo-100 text-indigo-700', + 'whisper' => 'bg-purple-100 text-purple-700', + default => 'bg-gray-100 text-gray-600', + }; + $labelServicio = match($log->servicio) { + 'gemini_intent' => 'Gemini Intent', + 'gemini_vision' => 'Gemini Vision', + 'whisper' => 'Whisper', + default => $log->servicio, + }; + @endphp + + + + + + + + + + + + + @empty + + + + @endforelse + +
FechaServicioCanalUsuarioTokens E/SAudioTiempoCostoEstadoDetalle
+ {{ $log->created_at->format('d/m/Y') }}
+ {{ $log->created_at->format('H:i:s') }} +
+ {{ $labelServicio }} + {{ $log->canal }} + {{ $log->usuario_id ? ('ID '.$log->usuario_id) : '—' }} + + @if($log->input_tokens || $log->output_tokens) + {{ number_format($log->input_tokens ?? 0) }} / {{ number_format($log->output_tokens ?? 0) }} + @else + — + @endif + + @if($log->audio_segundos) + {{ $log->audio_segundos }}s + @else + — + @endif + {{ number_format($log->tiempo_ms) }}ms + @if($log->costo > 0) + ${{ number_format($log->costo) }} + @else + — + @endif + + @if($log->resultado === 'ok') + OK + @else + Error + @endif + + @if(isset($detalle['texto']) && $detalle['texto']) + "{{ Str::limit($detalle['texto'], 60) }}" + @elseif(isset($detalle['accion']) && $detalle['accion']) + → {{ $detalle['accion'] }} + @elseif(isset($detalle['banco'])) + {{ $detalle['banco'] }} · ${{ number_format($detalle['valor'] ?? 0) }} + @elseif(isset($detalle['error'])) + {{ Str::limit($detalle['error'], 60) }} + @else + — + @endif +
+ No hay registros con los filtros seleccionados. +
+
+ @if($logs->hasPages()) +
+ {{ $logs->links() }} +
+ @endif +
+ +
diff --git a/resources/views/livewire/chat/show-configuracion-chat.blade.php b/resources/views/livewire/chat/show-configuracion-chat.blade.php old mode 100644 new mode 100755 index 8f964f1..c60d7d6 --- a/resources/views/livewire/chat/show-configuracion-chat.blade.php +++ b/resources/views/livewire/chat/show-configuracion-chat.blade.php @@ -109,6 +109,21 @@ Habilitar transcripción de audios en Telegram +
+ + @if($whisperResult) +

+ {{ $whisperResult }} +

+ @endif +
diff --git a/routes/web.php b/routes/web.php old mode 100755 new mode 100644 index 20ed613..ef45a46 --- a/routes/web.php +++ b/routes/web.php @@ -99,6 +99,7 @@ Route::middleware(["auth", "solo_usuario_administrador"])->group(function () { Route::get('/conversaciones', [ChatController::class, 'conversaciones'])->name('conversaciones'); Route::get('/menus', [ChatController::class, 'menus'])->name('menus'); Route::get('/configuracion', [ChatController::class, 'configuracion'])->name('configuracion'); + Route::get('/ia-logs', [ChatController::class, 'iaLogs'])->name('ia-logs'); }); });