Add AI usage logs for Gemini and Whisper
- Migration + AiUsageLog model with tokens, duration, time, cost fields - GeminiIntentService and GeminiVisionService log every API call with input/output token counts from usageMetadata and response time - TelegramBotService logs Whisper calls with audio duration (from Telegram) and calculates cost at $1/second - Whisper voice duration now passed from TelegramWebhookController - Free text in Telegram now tries Gemini intent detection before showing menu - /chat/ia-logs: Livewire component with summary cards + filterable table - Whisper connection test button in config panel - "Logs IA" link added to Chat/Bot nav section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
ff109a5608
commit
44297aa0bb
@@ -0,0 +1,180 @@
|
||||
<div class="space-y-6">
|
||||
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-800">Logs de uso de IA</h2>
|
||||
<p class="text-gray-500 text-sm">Historial de llamadas a Gemini y Whisper con tokens, tiempos y costos.</p>
|
||||
</div>
|
||||
|
||||
{{-- Cards resumen --}}
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow p-4">
|
||||
<p class="text-xs text-gray-400 mb-1">Total llamadas</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ number_format($resumen->total_llamadas ?? 0) }}</p>
|
||||
<p class="text-xs mt-1">
|
||||
<span class="text-emerald-600">{{ number_format($resumen->total_ok ?? 0) }} ok</span>
|
||||
·
|
||||
<span class="text-red-500">{{ number_format($resumen->total_errores ?? 0) }} errores</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow p-4">
|
||||
<p class="text-xs text-gray-400 mb-1">Tokens Gemini</p>
|
||||
<p class="text-2xl font-bold text-blue-700">{{ number_format(($resumen->total_input_tokens ?? 0) + ($resumen->total_output_tokens ?? 0)) }}</p>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
Entrada: {{ number_format($resumen->total_input_tokens ?? 0) }} · Salida: {{ number_format($resumen->total_output_tokens ?? 0) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow p-4">
|
||||
<p class="text-xs text-gray-400 mb-1">Audio Whisper</p>
|
||||
<p class="text-2xl font-bold text-purple-700">{{ number_format($resumen->total_segundos_whisper ?? 0) }}s</p>
|
||||
<p class="text-xs text-gray-400 mt-1">segundos transcritos</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow p-4">
|
||||
<p class="text-xs text-gray-400 mb-1">Costo Whisper</p>
|
||||
<p class="text-2xl font-bold text-emerald-700">${{ number_format($resumen->costo_whisper ?? 0) }}</p>
|
||||
<p class="text-xs text-gray-400 mt-1">$1 por segundo · Tiempo prom: {{ number_format($resumen->avg_tiempo_ms ?? 0) }}ms</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Filtros --}}
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow p-4">
|
||||
<div class="flex flex-wrap gap-3 items-end">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Servicio</label>
|
||||
<select wire:model.live="filtroServicio" class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none">
|
||||
<option value="">Todos</option>
|
||||
<option value="gemini_intent">Gemini Intent</option>
|
||||
<option value="gemini_vision">Gemini Vision</option>
|
||||
<option value="whisper">Whisper</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Canal</label>
|
||||
<select wire:model.live="filtroCanal" class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none">
|
||||
<option value="">Todos</option>
|
||||
<option value="web">Web</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Desde</label>
|
||||
<input wire:model.live="filtroFechaDesde" type="date" class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Hasta</label>
|
||||
<input wire:model.live="filtroFechaHasta" type="date" class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none">
|
||||
</div>
|
||||
<button wire:click="$set('filtroServicio','');$set('filtroCanal','');$set('filtroFechaDesde','');$set('filtroFechaHasta','')"
|
||||
class="px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 text-gray-600">
|
||||
Limpiar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Tabla --}}
|
||||
<div class="bg-white rounded-2xl border border-gray-100 shadow overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-gray-50 border-b border-gray-100">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 text-xs font-semibold text-gray-500">Fecha</th>
|
||||
<th class="text-left px-4 py-3 text-xs font-semibold text-gray-500">Servicio</th>
|
||||
<th class="text-left px-4 py-3 text-xs font-semibold text-gray-500">Canal</th>
|
||||
<th class="text-left px-4 py-3 text-xs font-semibold text-gray-500">Usuario</th>
|
||||
<th class="text-right px-4 py-3 text-xs font-semibold text-gray-500">Tokens E/S</th>
|
||||
<th class="text-right px-4 py-3 text-xs font-semibold text-gray-500">Audio</th>
|
||||
<th class="text-right px-4 py-3 text-xs font-semibold text-gray-500">Tiempo</th>
|
||||
<th class="text-right px-4 py-3 text-xs font-semibold text-gray-500">Costo</th>
|
||||
<th class="text-center px-4 py-3 text-xs font-semibold text-gray-500">Estado</th>
|
||||
<th class="text-left px-4 py-3 text-xs font-semibold text-gray-500">Detalle</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-50">
|
||||
@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
|
||||
<tr class="hover:bg-gray-50/50">
|
||||
<td class="px-4 py-3 text-gray-500 whitespace-nowrap text-xs">
|
||||
{{ $log->created_at->format('d/m/Y') }}<br>
|
||||
<span class="text-gray-400">{{ $log->created_at->format('H:i:s') }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold {{ $badgeServicio }}">{{ $labelServicio }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-600 capitalize text-xs">{{ $log->canal }}</td>
|
||||
<td class="px-4 py-3 text-gray-600 text-xs">
|
||||
{{ $log->usuario_id ? ('ID '.$log->usuario_id) : '—' }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-xs text-gray-600">
|
||||
@if($log->input_tokens || $log->output_tokens)
|
||||
{{ number_format($log->input_tokens ?? 0) }} / {{ number_format($log->output_tokens ?? 0) }}
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-xs text-gray-600">
|
||||
@if($log->audio_segundos)
|
||||
{{ $log->audio_segundos }}s
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-xs text-gray-600">{{ number_format($log->tiempo_ms) }}ms</td>
|
||||
<td class="px-4 py-3 text-right text-xs font-semibold {{ $log->costo > 0 ? 'text-emerald-700' : 'text-gray-400' }}">
|
||||
@if($log->costo > 0)
|
||||
${{ number_format($log->costo) }}
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
@if($log->resultado === 'ok')
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-700">OK</span>
|
||||
@else
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold bg-red-100 text-red-700">Error</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-500 max-w-xs">
|
||||
@if(isset($detalle['texto']) && $detalle['texto'])
|
||||
<span class="italic">"{{ Str::limit($detalle['texto'], 60) }}"</span>
|
||||
@elseif(isset($detalle['accion']) && $detalle['accion'])
|
||||
→ {{ $detalle['accion'] }}
|
||||
@elseif(isset($detalle['banco']))
|
||||
{{ $detalle['banco'] }} · ${{ number_format($detalle['valor'] ?? 0) }}
|
||||
@elseif(isset($detalle['error']))
|
||||
<span class="text-red-500">{{ Str::limit($detalle['error'], 60) }}</span>
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="10" class="px-4 py-12 text-center text-gray-400 text-sm">
|
||||
No hay registros con los filtros seleccionados.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if($logs->hasPages())
|
||||
<div class="px-4 py-3 border-t border-gray-100">
|
||||
{{ $logs->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
Reference in New Issue
Block a user