diff --git a/resources/views/livewire/summary/index.blade.php b/resources/views/livewire/summary/index.blade.php
index 6e72333..77844e8 100644
--- a/resources/views/livewire/summary/index.blade.php
+++ b/resources/views/livewire/summary/index.blade.php
@@ -11,17 +11,52 @@
use WithPagination;
public array $filters = [
- 'search' => null,
- 'model' => null,
- 'suite' => null,
+ 'gpu' => [],
+ 'model' => [],
+ 'quant' => [],
+ 'engine' => [],
+ 'suite' => [],
];
+ public function updatedFilters(): void
+ {
+ $this->resetPage();
+ }
+
public function resetFilters(): void
{
- $this->filters = ['search' => null, 'model' => null, 'suite' => null];
+ $this->filters = [
+ 'gpu' => [],
+ 'model' => [],
+ 'quant' => [],
+ 'engine' => [],
+ 'suite' => [],
+ ];
$this->resetPage();
}
+ public function toggleFilter(string $field, string $value): void
+ {
+ if (! array_key_exists($field, $this->filters)) {
+ return;
+ }
+
+ $this->filters[$field] = array_values(array_unique(
+ in_array($value, $this->filters[$field], true)
+ ? array_diff($this->filters[$field], [$value])
+ : array_merge($this->filters[$field], [$value])
+ ));
+
+ $this->resetPage();
+ }
+
+ public function filterLabel(string $field): string
+ {
+ $count = count($this->filters[$field] ?? []);
+
+ return $count === 0 ? '' : ' ('.$count.')';
+ }
+
#[Computed]
public function sessions()
{
@@ -63,34 +98,64 @@ private function bandQuery($user)
});
}
})
- ->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) {
+ ->when($this->filters['gpu'], fn ($q, array $gpus) => $q->whereIn('gpu', $gpus))
+ ->when($this->filters['model'], fn ($q, array $models) => $q->whereIn('model_id', $models))
+ ->when($this->filters['quant'], fn ($q, array $quants) => $q->whereIn('model_quant', $quants))
+ ->when($this->filters['engine'], fn ($q, array $engines) => $q->whereIn('engine', $engines))
+ ->when($this->filters['suite'], fn ($q, array $suites) => $q->whereExists(function ($sub) use ($suites) {
$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);
+ ->whereIn('session_suite_rows.suite', $suites);
}))
->orderByDesc('ran_at')
->orderBy('sequence');
}
+ #[Computed]
+ public function gpus(): array
+ {
+ return Cache::remember('summary:gpus', 300, function () {
+ return \Illuminate\Support\Facades\DB::table('session_summaries')
+ ->where('visibility', 'public')
+ ->whereNotNull('gpu')
+ ->distinct()->orderBy('gpu')->pluck('gpu')->all();
+ });
+ }
+
#[Computed]
public function models(): array
{
return Cache::remember('summary:models', 300, function () {
return \Illuminate\Support\Facades\DB::table('session_summaries')
->where('visibility', 'public')
+ ->whereNotNull('model_id')
->distinct()->orderBy('model_id')->pluck('model_id')->all();
});
}
+ #[Computed]
+ public function quants(): array
+ {
+ return Cache::remember('summary:quants', 300, function () {
+ return \Illuminate\Support\Facades\DB::table('session_summaries')
+ ->where('visibility', 'public')
+ ->whereNotNull('model_quant')
+ ->distinct()->orderBy('model_quant')->pluck('model_quant')->all();
+ });
+ }
+
+ #[Computed]
+ public function engines(): array
+ {
+ return Cache::remember('summary:engines', 300, function () {
+ return \Illuminate\Support\Facades\DB::table('session_summaries')
+ ->where('visibility', 'public')
+ ->whereNotNull('engine')
+ ->distinct()->orderBy('engine')->pluck('engine')->all();
+ });
+ }
+
#[Computed]
public function suites(): array
{
@@ -114,28 +179,43 @@ public function suites(): array
-
+ @php
+ $filterGroups = [
+ ['field' => 'gpu', 'label' => __('GPU'), 'options' => $this->gpus],
+ ['field' => 'model', 'label' => __('Model'), 'options' => $this->models],
+ ['field' => 'quant', 'label' => __('Quant'), 'options' => $this->quants],
+ ['field' => 'engine', 'label' => __('Inference engine'), 'options' => $this->engines],
+ ['field' => 'suite', 'label' => __('Suite'), 'options' => $this->suites],
+ ];
+ @endphp
-
- {{ __('All models') }}
- @foreach ($this->models as $model)
- {{ $model }}
- @endforeach
-
+ @foreach ($filterGroups as $group)
+
+
+ {{ $group['label'] }}{{ $this->filterLabel($group['field']) }}
+
+
-
- {{ __('All suites') }}
- @foreach ($this->suites as $suite)
- {{ $suite }}
- @endforeach
-
+
+ @foreach ($group['options'] as $option)
+ @php $checked = in_array($option, $this->filters[$group['field']], true); @endphp
+
+
+ @if ($checked)
+
+ @else
+
+ @endif
+ {{ $option }}
+
+
+ @endforeach
+
+
+ @endforeach
{{ __('Clear') }}
diff --git a/tests/Feature/SessionSummaryTest.php b/tests/Feature/SessionSummaryTest.php
index dd2dd6b..ddd242f 100644
--- a/tests/Feature/SessionSummaryTest.php
+++ b/tests/Feature/SessionSummaryTest.php
@@ -6,6 +6,7 @@
use App\Models\SoftbenchResult;
use App\Models\User;
use Illuminate\Support\Facades\Artisan;
+use Livewire\Volt\Volt;
test('festbench:summary splits runs by label into separate bands', function () {
$me = User::factory()->create(['name' => 'peterZ']);
@@ -158,3 +159,122 @@
->assertSee('All Task Rows')
->assertSee('flat-task');
});
+
+test('multi-filter: suite filter returns bands containing any selected suite', function () {
+ $me = User::factory()->create();
+
+ $soft = BenchmarkRun::factory()->create([
+ 'suite' => 'softbench', 'label' => 'soft-only', 'model_id' => 'm', 'model_quant' => 'Q5_K_M',
+ 'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
+ 'ran_at' => '2026-09-03 12:00:00',
+ ]);
+ SoftbenchResult::factory()->create(['benchmark_run_id' => $soft->id, 'ok' => true, 'user_tps' => 50.0, 'total' => 100, 'level' => 'off']);
+
+ $infra = BenchmarkRun::factory()->create([
+ 'suite' => 'infrabench', 'label' => 'infra-only', 'model_id' => 'm', 'model_quant' => 'Q5_K_M',
+ 'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
+ 'ran_at' => '2026-09-03 12:05:00',
+ ]);
+ InfrabenchResult::factory()->create(['benchmark_run_id' => $infra->id, 'ok' => true, 'user_tps' => 30.0, 'total' => 100, 'level' => 'low']);
+
+ Artisan::call('festbench:summary');
+
+ // No filter: both bands visible.
+ Volt::test('summary.index')->assertSee('soft-only')->assertSee('infra-only');
+
+ // Filter to infrabench: only the infra band should match.
+ Volt::test('summary.index', ['filters' => [
+ 'gpu' => [], 'model' => [], 'quant' => [], 'engine' => [], 'suite' => ['infrabench'],
+ ]])
+ ->assertDontSee('soft-only')
+ ->assertSee('infra-only');
+});
+
+test('multi-filter: AND across categories, OR within a category', function () {
+ $me = User::factory()->create();
+
+ // Band A: GPU1 + model-A + Q5_K_M + llama.cpp
+ $a = BenchmarkRun::factory()->create([
+ 'suite' => 'softbench', 'label' => 'band-a', 'model_id' => 'model-A', 'model_quant' => 'Q5_K_M',
+ 'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
+ 'ran_at' => '2026-09-03 12:00:00',
+ ]);
+ SoftbenchResult::factory()->create(['benchmark_run_id' => $a->id, 'ok' => true, 'user_tps' => 50.0, 'total' => 100, 'level' => 'off']);
+
+ // Band B: GPU2 + model-B + UD-Q4_K_M + ollama-cloud
+ $b = BenchmarkRun::factory()->create([
+ 'suite' => 'softbench', 'label' => 'band-b', 'model_id' => 'model-B', 'model_quant' => 'UD-Q4_K_M',
+ 'gpu' => 'GPU2', 'gpu_count' => 1, 'engine' => 'ollama-cloud', 'user_id' => $me->id, 'visibility' => 'public',
+ 'ran_at' => '2026-09-04 12:00:00',
+ ]);
+ SoftbenchResult::factory()->create(['benchmark_run_id' => $b->id, 'ok' => true, 'user_tps' => 40.0, 'total' => 100, 'level' => 'off']);
+
+ Artisan::call('festbench:summary');
+
+ // GPU filter alone: OR within GPU → both bands.
+ Volt::test('summary.index', ['filters' => [
+ 'gpu' => ['GPU1', 'GPU2'], 'model' => [], 'quant' => [], 'engine' => [], 'suite' => [],
+ ]])
+ ->assertSee('band-a')
+ ->assertSee('band-b');
+
+ // GPU + model: AND across → only matching band.
+ Volt::test('summary.index', ['filters' => [
+ 'gpu' => ['GPU1', 'GPU2'], 'model' => ['model-A'], 'quant' => [], 'engine' => [], 'suite' => [],
+ ]])
+ ->assertSee('band-a')
+ ->assertDontSee('band-b');
+
+ // Engine filter: only llama.cpp band.
+ Volt::test('summary.index', ['filters' => [
+ 'gpu' => [], 'model' => [], 'quant' => [], 'engine' => ['llama.cpp'], 'suite' => [],
+ ]])
+ ->assertSee('band-a')
+ ->assertDontSee('band-b');
+
+ // Quant filter: only UD-Q4_K_M band.
+ Volt::test('summary.index', ['filters' => [
+ 'gpu' => [], 'model' => [], 'quant' => ['UD-Q4_K_M'], 'engine' => [], 'suite' => [],
+ ]])
+ ->assertDontSee('band-a')
+ ->assertSee('band-b');
+});
+
+test('multi-filter: multiple suites use OR for band inclusion', function () {
+ $me = User::factory()->create();
+
+ // Band with only softbench.
+ $soft = BenchmarkRun::factory()->create([
+ 'suite' => 'softbench', 'label' => 'has-soft', 'model_id' => 'm', 'model_quant' => 'Q5_K_M',
+ 'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
+ 'ran_at' => '2026-09-03 12:00:00',
+ ]);
+ SoftbenchResult::factory()->create(['benchmark_run_id' => $soft->id, 'ok' => true, 'user_tps' => 50.0, 'total' => 100, 'level' => 'off']);
+
+ // Band with only infrabench.
+ $infra = BenchmarkRun::factory()->create([
+ 'suite' => 'infrabench', 'label' => 'has-infra', 'model_id' => 'm', 'model_quant' => 'Q5_K_M',
+ 'gpu' => 'GPU1', 'gpu_count' => 1, 'engine' => 'llama.cpp', 'user_id' => $me->id, 'visibility' => 'public',
+ 'ran_at' => '2026-09-03 12:05:00',
+ ]);
+ InfrabenchResult::factory()->create(['benchmark_run_id' => $infra->id, 'ok' => true, 'user_tps' => 30.0, 'total' => 100, 'level' => 'low']);
+
+ Artisan::call('festbench:summary');
+
+ // Both suites selected → both bands visible (OR within suite).
+ Volt::test('summary.index', ['filters' => [
+ 'gpu' => [], 'model' => [], 'quant' => [], 'engine' => [], 'suite' => ['softbench', 'infrabench'],
+ ]])
+ ->assertSee('has-soft')
+ ->assertSee('has-infra');
+});
+
+test('toggleFilter adds and removes a value from a filter array', function () {
+ $component = Volt::test('summary.index');
+
+ $component->call('toggleFilter', 'suite', 'infrabench')
+ ->assertSet('filters.suite', ['infrabench']);
+
+ $component->call('toggleFilter', 'suite', 'infrabench')
+ ->assertSet('filters.suite', []);
+});