festbench-web/resources/views/livewire/summary/index.blade.php
PeterChrz 895a764b88
ui: group suite rows under their devlogic/sysops total header
Each category now renders as a self-contained section: its colored
total strip, its own column headings, and its suite rows directly
beneath. Empty categories are omitted entirely. Removes the old
single-column-header layout that sat above all rows regardless of
category.
2026-10-05 00:20:19 -04:00

302 lines
No EOL
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Models\BenchmarkRun;
use Illuminate\Support\Facades\Cache;
use Livewire\Attributes\Computed;
use Livewire\Component;
use Livewire\WithPagination;
new class extends Component
{
use WithPagination;
public array $filters = [
'search' => null,
'model' => null,
'suite' => null,
];
public function resetFilters(): void
{
$this->filters = ['search' => null, 'model' => null, 'suite' => null];
$this->resetPage();
}
#[Computed]
public function sessions()
{
$ttl = (int) config('festbench.home_cache_ttl', 300);
$user = auth()->user();
// Guest views are shared-cacheable; logged-in views may contain the
// user's private bands, so they get a user-scoped key.
$cacheKey = 'summary:'.($user?->id ?? 'guest').':'.md5(json_encode([$this->filters, $this->getPage()]));
return Cache::remember($cacheKey, $ttl, function () use ($user) {
$bands = $this->bandQuery($user)->paginate(10);
$runIds = $bands->getCollection()->map(fn ($b) => $b->id)->all();
$suiteRows = $runIds === []
? collect()
: \Illuminate\Support\Facades\DB::table('session_suite_rows')
->whereIn('session_summary_id', $runIds)
->orderBy('sequence')
->get()
->groupBy('session_summary_id');
$bands->getCollection()->transform(fn ($band) => (object) [
'band' => $band,
'suites' => $suiteRows->get($band->id, collect()),
]);
return $bands;
});
}
private function bandQuery($user)
{
return \Illuminate\Support\Facades\DB::table('session_summaries')
->where(function ($q) use ($user) {
$q->where('visibility', 'public');
if ($user !== null) {
$q->orWhere(function ($own) use ($user) {
$own->where('visibility', 'private')->where('user_id', $user->id);
});
}
})
->when($this->filters['search'], fn ($q, $search) => $q->where(function ($q) use ($search) {
$q->where('label', 'like', "%{$search}%")
->orWhere('model_id', 'like', "%{$search}%")
->orWhere('gpu', 'like', "%{$search}%")
->orWhere('runner_name', 'like', "%{$search}%");
}))
->when($this->filters['model'], fn ($q, $model) => $q->where('model_id', $model))
// Suite filter: keep bands containing the chosen suite
->when($this->filters['suite'], fn ($q, $suite) => $q->whereExists(function ($sub) use ($suite) {
$sub->select(\Illuminate\Support\Facades\DB::raw(1))
->from('session_suite_rows')
->whereColumn('session_suite_rows.session_summary_id', 'session_summaries.id')
->where('session_suite_rows.suite', $suite);
}))
->orderByDesc('ran_at')
->orderBy('sequence');
}
#[Computed]
public function models(): array
{
return Cache::remember('summary:models', 300, function () {
return \Illuminate\Support\Facades\DB::table('session_summaries')
->where('visibility', 'public')
->distinct()->orderBy('model_id')->pluck('model_id')->all();
});
}
#[Computed]
public function suites(): array
{
return Cache::remember('summary:suites', 300, function () {
return \Illuminate\Support\Facades\DB::table('session_suite_rows')
->join('session_summaries', 'session_summaries.id', '=', 'session_suite_rows.session_summary_id')
->where('session_summaries.visibility', 'public')
->distinct()->orderBy('suite')->pluck('suite')->all();
});
}
};
?>
<div>
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ __('FestBench - Benchmarks') }}</flux:heading>
<flux:subheading>{{ __('GPU, model, suite scores — click a suite line for per-task detail.') }}</flux:subheading>
</div>
<flux:button variant="ghost" icon="table-cells" href="{{ route('results') }}" wire:navigate>{{ __('All task rows') }}</flux:button>
</div>
<div class="mt-6 flex flex-wrap items-end gap-3">
<flux:input
wire:model.live.debounce.300ms="filters.search"
:label="__('Search')"
:placeholder="__('Session, model, GPU or runner…')"
size="sm"
class="w-64"
clearable
/>
<flux:select wire:model.live="filters.model" :label="__('Model')" size="sm" class="w-56">
<flux:select.option value="">{{ __('All models') }}</flux:select.option>
@foreach ($this->models as $model)
<flux:select.option value="{{ $model }}">{{ $model }}</flux:select.option>
@endforeach
</flux:select>
<flux:select wire:model.live="filters.suite" :label="__('Suite')" size="sm" class="w-44">
<flux:select.option value="">{{ __('All suites') }}</flux:select.option>
@foreach ($this->suites as $suite)
<flux:select.option value="{{ $suite }}">{{ $suite }}</flux:select.option>
@endforeach
</flux:select>
<flux:button size="sm" variant="ghost" wire:click="resetFilters" icon="x-mark">{{ __('Clear') }}</flux:button>
</div>
@if ($this->sessions->count() > 0)
<div class="mt-6 flex flex-col gap-4">
@foreach ($this->sessions as $session)
<flux:card class="!p-0">
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 border-b border-zinc-200 px-4 py-3 dark:border-zinc-700">
<flux:icon name="cpu" class="size-4 text-zinc-500" />
<span class="text-sm font-semibold">{{ $session->band->gpu ?? 'unknown GPU' }}</span>
@if ($session->band->gpu_count)
<flux:badge size="xs" color="zinc">×{{ $session->band->gpu_count }}</flux:badge>
@endif
<span class="text-sm">{{ $session->band->model_id ?? 'unknown model' }}</span>
@if ($session->band->label)
<flux:badge size="xs" color="amber">{{ $session->band->label }}</flux:badge>
@endif
@if ($session->band->model_quant)
<flux:badge size="xs" color="zinc">{{ $session->band->model_quant }}</flux:badge>
@endif
@if ($session->band->engine)
<flux:badge size="xs" color="sky">{{ $session->band->engine }}</flux:badge>
@endif
<flux:spacer />
<span class="text-sm text-zinc-500">{{ $session->band->runner_name ?? 'anonymous' }}</span>
<span class="text-xs text-zinc-400">{{ optional($session->band->ran_at)->format('M j Y') }}</span>
</div>
@php
$devlogicSuites = ['softbench', 'prosebench', 'psychbench', 'depthsweep'];
$sysopsSuites = ['infrabench', 'scenariobench'];
$devlogicRows = $session->suites->filter(fn ($r) => in_array($r->suite, $devlogicSuites));
$sysopsRows = $session->suites->filter(fn ($r) => in_array($r->suite, $sysopsSuites));
@endphp
{{-- Devlogic section: total strip + column headers + suite rows --}}
@if ($devlogicRows->isNotEmpty() && $session->band->devlogic_tps !== null)
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 border-l-2 border-sky-400 bg-sky-50/50 px-4 py-2 dark:border-sky-500 dark:bg-sky-950/20">
<span class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400">{{ __('Devlogic total') }}</span>
<span class="text-sm">
<span class="font-mono font-semibold">{{ number_format($session->band->devlogic_tps, 1) }}</span>
<span class="text-xs text-zinc-400">{{ __('tok/s') }}</span>
</span>
<span class="text-sm">
<span class="font-mono font-semibold">{{ number_format($session->band->devlogic_tokens) }}</span>
<span class="text-xs text-zinc-400">{{ __('tokens') }}</span>
</span>
<span class="text-sm">
<flux:badge size="xs" color="emerald">✔ {{ $session->band->devlogic_passed }}</flux:badge>
<flux:badge size="xs" color="{{ $session->band->devlogic_failed > 0 ? 'rose' : 'zinc' }}">✘ {{ $session->band->devlogic_failed }}</flux:badge>
</span>
</div>
<div class="hidden px-4 pt-2 md:grid md:grid-cols-[7.5rem_4.5rem_7rem_15rem_6rem_1fr] md:gap-x-4 md:items-center">
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Benchmark') }}</span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Thinking') }}</span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Result') }}</span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Speed (tok/s)') }} <span class="font-normal normal-case">{{ __('weighted average') }}</span></span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Tokens') }}</span>
<span></span>
</div>
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
@foreach ($devlogicRows as $row)
@php
$detailUrl = $row->suite === 'scenariobench'
? route('sessions.scenarios', $session->band->id)
: route('runs.show', $row->run_id) . ($row->level && $row->suite === 'depthsweep' ? '?level=' . $row->level : '');
@endphp
<a href="{{ $detailUrl }}" wire:navigate
class="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-2.5 hover:bg-zinc-50 dark:hover:bg-zinc-800/60 md:grid md:grid-cols-[7.5rem_4.5rem_7rem_15rem_6rem_1fr] md:gap-x-4 md:items-center">
<span><flux:badge size="xs">{{ $row->suite }}</flux:badge></span>
<span>
@if ($row->level)
<flux:badge size="xs" color="sky">{{ $row->level }}</flux:badge>
@endif
</span>
<span class="flex items-center gap-1.5">
<flux:badge size="xs" color="emerald">✔ {{ $row->passed }}</flux:badge>
<flux:badge size="xs" color="{{ $row->failed > 0 ? 'rose' : 'zinc' }}">✘ {{ $row->failed }}</flux:badge>
</span>
<span>
@if ($row->weighted_user_tps !== null)
<span class="font-mono text-sm">{{ number_format($row->weighted_user_tps, 1) }}</span>
@elseif ($row->gen_tps !== null)
<span class="font-mono text-sm">{{ number_format($row->gen_tps, 1) }} <span class="text-xs text-zinc-400">{{ __('gen') }}</span></span>
@endif
</span>
<span class="font-mono text-sm">{{ number_format($row->total_tokens) }}</span>
<span class="text-xs text-zinc-400 md:justify-self-end">{{ __('per-task detail') }} →</span>
</a>
@endforeach
</div>
@endif
{{-- Sysops section: total strip + column headers + suite rows --}}
@if ($sysopsRows->isNotEmpty() && $session->band->sysops_tps !== null)
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 border-l-2 border-amber-400 bg-amber-50/50 px-4 py-2 dark:border-amber-500 dark:bg-amber-950/20">
<span class="text-xs font-semibold uppercase tracking-wider text-amber-600 dark:text-amber-400">{{ __('Sysops total') }}</span>
<span class="text-sm">
<span class="font-mono font-semibold">{{ number_format($session->band->sysops_tps, 1) }}</span>
<span class="text-xs text-zinc-400">{{ __('tok/s') }}</span>
</span>
<span class="text-sm">
<span class="font-mono font-semibold">{{ number_format($session->band->sysops_tokens) }}</span>
<span class="text-xs text-zinc-400">{{ __('tokens') }}</span>
</span>
<span class="text-sm">
<flux:badge size="xs" color="emerald">✔ {{ $session->band->sysops_passed }}</flux:badge>
<flux:badge size="xs" color="{{ $session->band->sysops_failed > 0 ? 'rose' : 'zinc' }}">✘ {{ $session->band->sysops_failed }}</flux:badge>
</span>
</div>
<div class="hidden px-4 pt-2 md:grid md:grid-cols-[7.5rem_4.5rem_7rem_15rem_6rem_1fr] md:gap-x-4 md:items-center">
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Benchmark') }}</span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Thinking') }}</span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Result') }}</span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Speed (tok/s)') }} <span class="font-normal normal-case">{{ __('weighted average') }}</span></span>
<span class="text-[10px] font-semibold uppercase tracking-wider text-zinc-400">{{ __('Tokens') }}</span>
<span></span>
</div>
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
@foreach ($sysopsRows as $row)
@php
$detailUrl = $row->suite === 'scenariobench'
? route('sessions.scenarios', $session->band->id)
: route('runs.show', $row->run_id) . ($row->level && $row->suite === 'depthsweep' ? '?level=' . $row->level : '');
@endphp
<a href="{{ $detailUrl }}" wire:navigate
class="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-2.5 hover:bg-zinc-50 dark:hover:bg-zinc-800/60 md:grid md:grid-cols-[7.5rem_4.5rem_7rem_15rem_6rem_1fr] md:gap-x-4 md:items-center">
<span><flux:badge size="xs">{{ $row->suite }}</flux:badge></span>
<span>
@if ($row->level)
<flux:badge size="xs" color="sky">{{ $row->level }}</flux:badge>
@endif
</span>
<span class="flex items-center gap-1.5">
<flux:badge size="xs" color="emerald">✔ {{ $row->passed }}</flux:badge>
<flux:badge size="xs" color="{{ $row->failed > 0 ? 'rose' : 'zinc' }}">✘ {{ $row->failed }}</flux:badge>
</span>
<span>
@if ($row->weighted_user_tps !== null)
<span class="font-mono text-sm">{{ number_format($row->weighted_user_tps, 1) }}</span>
@elseif ($row->gen_tps !== null)
<span class="font-mono text-sm">{{ number_format($row->gen_tps, 1) }} <span class="text-xs text-zinc-400">{{ __('gen') }}</span></span>
@endif
</span>
<span class="font-mono text-sm">{{ number_format($row->total_tokens) }}</span>
<span class="text-xs text-zinc-400 md:justify-self-end">{{ __('per-task detail') }} →</span>
</a>
@endforeach
</div>
@endif
</flux:card>
@endforeach
</div>
<div class="mt-4">
{{ $this->sessions->links() }}
</div>
@else
<div class="mt-6">
<flux:callout variant="brand">
<flux:callout.heading>{{ __('No sessions') }}</flux:callout.heading>
<flux:callout.text>{{ __('No benchmark sessions match the current filters.') }}</flux:callout.text>
</flux:callout>
</div>
@endif
</div>