feat: log IA con filtro por mes y costo por petición + fix promos sin tarifaPromo
- ShowAiLogs: filtro rápido por mes (YYYY-MM) mutuamente excluyente con rango de fechas
- Resumen IA: nueva card Costo Gemini y Costo Total (Gemini + Whisper)
- Tabla IA: columna Costo COP calcula (tokens/1M × $20.000) para Gemini, costo guardado para Whisper
- PublicChat + TelegramBotService: quita whereHas('tarifaPromo') que ocultaba promos sin tarifa por rol
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
343baa9c14
commit
5c7c8c8a95
@@ -906,7 +906,6 @@ class PublicChat extends Component
|
|||||||
|
|
||||||
$promos = Promociones::where('visible', 'true')
|
$promos = Promociones::where('visible', 'true')
|
||||||
->where(fn ($q) => $q->whereNull('fecha_limite')->orWhere('fecha_limite', '>=', now()))
|
->where(fn ($q) => $q->whereNull('fecha_limite')->orWhere('fecha_limite', '>=', now()))
|
||||||
->whereHas('tarifaPromo', fn ($q) => $q->where('rol_id', $rolId))
|
|
||||||
->with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])
|
->with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
+57
-18
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Livewire\Chat;
|
namespace App\Http\Livewire\Chat;
|
||||||
|
|
||||||
use App\Models\AiUsageLog;
|
use App\Models\AiUsageLog;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\WithPagination;
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
@@ -10,43 +11,72 @@ class ShowAiLogs extends Component
|
|||||||
{
|
{
|
||||||
use WithPagination;
|
use WithPagination;
|
||||||
|
|
||||||
public string $filtroServicio = '';
|
public string $filtroServicio = '';
|
||||||
public string $filtroCanal = '';
|
public string $filtroCanal = '';
|
||||||
public string $filtroFechaDesde = '';
|
public string $filtroFechaDesde = '';
|
||||||
public string $filtroFechaHasta = '';
|
public string $filtroFechaHasta = '';
|
||||||
|
public string $filtroMes = '';
|
||||||
|
|
||||||
protected $queryString = ['filtroServicio', 'filtroCanal', 'filtroFechaDesde', 'filtroFechaHasta'];
|
protected $queryString = [
|
||||||
|
'filtroServicio', 'filtroCanal',
|
||||||
|
'filtroFechaDesde', 'filtroFechaHasta',
|
||||||
|
'filtroMes',
|
||||||
|
];
|
||||||
|
|
||||||
public function updatingFiltroServicio(): void { $this->resetPage(); }
|
public function updatingFiltroServicio(): void { $this->resetPage(); }
|
||||||
public function updatingFiltroCanal(): void { $this->resetPage(); }
|
public function updatingFiltroCanal(): void { $this->resetPage(); }
|
||||||
public function updatingFiltroFechaDesde(): void { $this->resetPage(); }
|
public function updatingFiltroFechaDesde(): void { $this->resetPage(); }
|
||||||
public function updatingFiltroFechaHasta(): void { $this->resetPage(); }
|
public function updatingFiltroFechaHasta(): void { $this->resetPage(); }
|
||||||
|
public function updatingFiltroMes(): void { $this->resetPage(); }
|
||||||
|
|
||||||
public function render()
|
public function updatedFiltroMes(): void
|
||||||
{
|
{
|
||||||
$query = AiUsageLog::query()->latest();
|
if ($this->filtroMes) {
|
||||||
|
$this->filtroFechaDesde = '';
|
||||||
|
$this->filtroFechaHasta = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedFiltroFechaDesde(): void
|
||||||
|
{
|
||||||
|
if ($this->filtroFechaDesde) $this->filtroMes = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedFiltroFechaHasta(): void
|
||||||
|
{
|
||||||
|
if ($this->filtroFechaHasta) $this->filtroMes = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyFiltros($query): void
|
||||||
|
{
|
||||||
if ($this->filtroServicio) {
|
if ($this->filtroServicio) {
|
||||||
$query->where('servicio', $this->filtroServicio);
|
$query->where('servicio', $this->filtroServicio);
|
||||||
}
|
}
|
||||||
if ($this->filtroCanal) {
|
if ($this->filtroCanal) {
|
||||||
$query->where('canal', $this->filtroCanal);
|
$query->where('canal', $this->filtroCanal);
|
||||||
}
|
}
|
||||||
if ($this->filtroFechaDesde) {
|
if ($this->filtroMes) {
|
||||||
$query->whereDate('created_at', '>=', $this->filtroFechaDesde);
|
$inicio = Carbon::createFromFormat('Y-m', $this->filtroMes)->startOfMonth();
|
||||||
}
|
$fin = $inicio->copy()->endOfMonth();
|
||||||
if ($this->filtroFechaHasta) {
|
$query->whereBetween('created_at', [$inicio, $fin]);
|
||||||
$query->whereDate('created_at', '<=', $this->filtroFechaHasta);
|
} else {
|
||||||
|
if ($this->filtroFechaDesde) {
|
||||||
|
$query->whereDate('created_at', '>=', $this->filtroFechaDesde);
|
||||||
|
}
|
||||||
|
if ($this->filtroFechaHasta) {
|
||||||
|
$query->whereDate('created_at', '<=', $this->filtroFechaHasta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$query = AiUsageLog::query()->latest();
|
||||||
|
$this->applyFiltros($query);
|
||||||
$logs = $query->paginate(30);
|
$logs = $query->paginate(30);
|
||||||
|
|
||||||
// Resumen total (aplicando mismos filtros sin paginación)
|
|
||||||
$resumenQuery = AiUsageLog::query();
|
$resumenQuery = AiUsageLog::query();
|
||||||
if ($this->filtroServicio) $resumenQuery->where('servicio', $this->filtroServicio);
|
$this->applyFiltros($resumenQuery);
|
||||||
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("
|
$resumen = $resumenQuery->selectRaw("
|
||||||
COUNT(*) as total_llamadas,
|
COUNT(*) as total_llamadas,
|
||||||
@@ -56,9 +86,18 @@ class ShowAiLogs extends Component
|
|||||||
SUM(output_tokens) as total_output_tokens,
|
SUM(output_tokens) as total_output_tokens,
|
||||||
SUM(audio_segundos) as total_segundos_whisper,
|
SUM(audio_segundos) as total_segundos_whisper,
|
||||||
SUM(CASE WHEN servicio = 'whisper' THEN costo ELSE 0 END) as costo_whisper,
|
SUM(CASE WHEN servicio = 'whisper' THEN costo ELSE 0 END) as costo_whisper,
|
||||||
|
SUM((COALESCE(input_tokens,0) + COALESCE(output_tokens,0)) / 1000000.0 * 20000) as costo_gemini,
|
||||||
AVG(tiempo_ms) as avg_tiempo_ms
|
AVG(tiempo_ms) as avg_tiempo_ms
|
||||||
")->first();
|
")->first();
|
||||||
|
|
||||||
return view('livewire.chat.show-ai-logs', compact('logs', 'resumen'));
|
$meses = collect(range(0, 12))->map(function ($i) {
|
||||||
|
$d = now()->subMonths($i);
|
||||||
|
return [
|
||||||
|
'value' => $d->format('Y-m'),
|
||||||
|
'label' => ucfirst($d->locale('es')->isoFormat('MMMM YYYY')),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return view('livewire.chat.show-ai-logs', compact('logs', 'resumen', 'meses'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -925,7 +925,6 @@ class TelegramBotService
|
|||||||
|
|
||||||
$promos = Promociones::where('visible', 'true')
|
$promos = Promociones::where('visible', 'true')
|
||||||
->where(fn ($q) => $q->whereNull('fecha_limite')->orWhere('fecha_limite', '>=', now()))
|
->where(fn ($q) => $q->whereNull('fecha_limite')->orWhere('fecha_limite', '>=', now()))
|
||||||
->whereHas('tarifaPromo', fn ($q) => $q->where('rol_id', $rolId))
|
|
||||||
->with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])
|
->with(['tarifaPromo' => fn ($q) => $q->where('rol_id', $rolId)])
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
+44
-18
@@ -6,7 +6,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Cards resumen --}}
|
{{-- Cards resumen --}}
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
@php
|
||||||
|
$costoGemini = $resumen->costo_gemini ?? 0;
|
||||||
|
$costoWhisper = $resumen->costo_whisper ?? 0;
|
||||||
|
$costoTotal = $costoGemini + $costoWhisper;
|
||||||
|
@endphp
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||||
<div class="bg-white rounded-2xl border border-gray-100 shadow p-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-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-2xl font-bold text-gray-800">{{ number_format($resumen->total_llamadas ?? 0) }}</p>
|
||||||
@@ -20,7 +25,7 @@
|
|||||||
<p class="text-xs text-gray-400 mb-1">Tokens Gemini</p>
|
<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-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">
|
<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) }}
|
E: {{ number_format($resumen->total_input_tokens ?? 0) }} · S: {{ number_format($resumen->total_output_tokens ?? 0) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-2xl border border-gray-100 shadow p-4">
|
<div class="bg-white rounded-2xl border border-gray-100 shadow p-4">
|
||||||
@@ -29,15 +34,41 @@
|
|||||||
<p class="text-xs text-gray-400 mt-1">segundos transcritos</p>
|
<p class="text-xs text-gray-400 mt-1">segundos transcritos</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-2xl border border-gray-100 shadow p-4">
|
<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-xs text-gray-400 mb-1">Costo Gemini</p>
|
||||||
<p class="text-2xl font-bold text-emerald-700">${{ number_format($resumen->costo_whisper ?? 0) }}</p>
|
<p class="text-2xl font-bold text-blue-700">${{ number_format($costoGemini, 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>
|
<p class="text-xs text-gray-400 mt-1">$20.000 / millón tokens</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 Total</p>
|
||||||
|
<p class="text-2xl font-bold text-emerald-700">${{ number_format($costoTotal, 0, ',', '.') }}</p>
|
||||||
|
<p class="text-xs text-gray-400 mt-1">
|
||||||
|
Whisper: ${{ number_format($costoWhisper, 0, ',', '.') }}
|
||||||
|
· Prom: {{ number_format($resumen->avg_tiempo_ms ?? 0) }}ms
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Filtros --}}
|
{{-- Filtros --}}
|
||||||
<div class="bg-white rounded-2xl border border-gray-100 shadow p-4">
|
<div class="bg-white rounded-2xl border border-gray-100 shadow p-4">
|
||||||
<div class="flex flex-wrap gap-3 items-end">
|
<div class="flex flex-wrap gap-3 items-end">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Mes</label>
|
||||||
|
<select wire:model.live="filtroMes" 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 los meses</option>
|
||||||
|
@foreach($meses as $mes)
|
||||||
|
<option value="{{ $mes['value'] }}">{{ $mes['label'] }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end gap-1 pb-2 text-gray-400 text-xs">ó rango:</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>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Servicio</label>
|
<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">
|
<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">
|
||||||
@@ -55,15 +86,7 @@
|
|||||||
<option value="telegram">Telegram</option>
|
<option value="telegram">Telegram</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<button wire:click="$set('filtroServicio','');$set('filtroCanal','');$set('filtroFechaDesde','');$set('filtroFechaHasta','');$set('filtroMes','')"
|
||||||
<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">
|
class="px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 text-gray-600">
|
||||||
Limpiar
|
Limpiar
|
||||||
</button>
|
</button>
|
||||||
@@ -83,7 +106,7 @@
|
|||||||
<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">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">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">Tiempo</th>
|
||||||
<th class="text-right px-4 py-3 text-xs font-semibold text-gray-500">Costo</th>
|
<th class="text-right px-4 py-3 text-xs font-semibold text-gray-500">Costo COP</th>
|
||||||
<th class="text-center px-4 py-3 text-xs font-semibold text-gray-500">Estado</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>
|
<th class="text-left px-4 py-3 text-xs font-semibold text-gray-500">Detalle</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -104,6 +127,9 @@
|
|||||||
'whisper' => 'Whisper',
|
'whisper' => 'Whisper',
|
||||||
default => $log->servicio,
|
default => $log->servicio,
|
||||||
};
|
};
|
||||||
|
$costoFila = in_array($log->servicio, ['gemini_intent', 'gemini_vision'])
|
||||||
|
? (($log->input_tokens ?? 0) + ($log->output_tokens ?? 0)) / 1000000 * 20000
|
||||||
|
: ($log->costo ?? 0);
|
||||||
@endphp
|
@endphp
|
||||||
<tr class="hover:bg-gray-50/50">
|
<tr class="hover:bg-gray-50/50">
|
||||||
<td class="px-4 py-3 text-gray-500 whitespace-nowrap text-xs">
|
<td class="px-4 py-3 text-gray-500 whitespace-nowrap text-xs">
|
||||||
@@ -132,9 +158,9 @@
|
|||||||
@endif
|
@endif
|
||||||
</td>
|
</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 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' }}">
|
<td class="px-4 py-3 text-right text-xs font-semibold {{ $costoFila > 0 ? 'text-emerald-700' : 'text-gray-400' }}">
|
||||||
@if($log->costo > 0)
|
@if($costoFila > 0)
|
||||||
${{ number_format($log->costo) }}
|
${{ number_format($costoFila, 0, ',', '.') }}
|
||||||
@else
|
@else
|
||||||
—
|
—
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
Reference in New Issue
Block a user