festbench-web/resources/views/livewire/my-runs/index.blade.php

222 lines
No EOL
10 KiB
PHP

<?php
use App\Models\BenchmarkRun;
use App\Models\Gpu;
use Flux\Flux;
use Illuminate\Pagination\LengthAwarePaginator;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithPagination;
new #[Layout('components.layouts.app')] class extends Component {
use WithPagination;
public array $filters = [
'search' => null,
];
public function toggleVisibility(int $id): void
{
$run = BenchmarkRun::where('user_id', auth()->id())->findOrFail($id);
$run->fill([
'visibility' => $run->visibility === BenchmarkRun::VISIBILITY_PUBLIC
? BenchmarkRun::VISIBILITY_PRIVATE
: BenchmarkRun::VISIBILITY_PUBLIC,
])->save();
Flux::toast(variant: 'success', text: __('Visibility updated.'));
}
// --- GPU confirmation -------------------------------------------------
// Runs whose raw gpu string has no catalog match carry gpu_id = null.
// The owner confirms them here; confirming links gpu_id and re-runs the
// summary so the run retro-merges into the correct session band.
public ?int $confirmingRunId = null;
public string $gpuSearch = '';
#[Computed]
public function gpuChoices(): array
{
if ($this->gpuSearch === '') {
return Gpu::active()->orderBy('vendor')->orderBy('name')->limit(10)->pluck('name', 'id')->all();
}
return Gpu::active()
->where('name', 'like', "%{$this->gpuSearch}%")
->orderBy('name')
->limit(10)
->pluck('name', 'id')
->all();
}
public function startGpuConfirm(int $id): void
{
$this->confirmingRunId = $id;
$this->gpuSearch = '';
$this->modal('gpu-confirm-modal')->show();
}
public function confirmGpu(int $gpuId): void
{
$run = BenchmarkRun::where('user_id', auth()->id())->findOrFail($this->confirmingRunId);
$gpu = Gpu::active()->findOrFail($gpuId);
$run->fill(['gpu' => $gpu->name, 'gpu_id' => $gpu->id])->save();
// Historical regroup: the run moves into the correct session band.
Illuminate\Support\Facades\Artisan::call('festbench:summary');
Illuminate\Support\Facades\Artisan::call('festbench:snapshot');
$this->modal('gpu-confirm-modal')->close();
$this->confirmingRunId = null;
Flux::toast(variant: 'success', text: __('GPU confirmed — run regrouped.'));
}
public function resetFilters(): void
{
$this->filters = ['search' => null];
$this->resetPage();
}
#[Computed]
public function runs(): LengthAwarePaginator
{
// Row + pass counts across the suite tables (runs are single-suite,
// but the owning table varies per run, hence the union of counts).
return BenchmarkRun::query()
->where('user_id', auth()->id())
->select('benchmark_runs.*')
->selectRaw('(SELECT count(*) FROM softbench_results sr WHERE sr.benchmark_run_id = benchmark_runs.id)
+ (SELECT count(*) FROM infrabench_results sr WHERE sr.benchmark_run_id = benchmark_runs.id)
+ (SELECT count(*) FROM prosebench_results sr WHERE sr.benchmark_run_id = benchmark_runs.id)
+ (SELECT count(*) FROM psychbench_results sr WHERE sr.benchmark_run_id = benchmark_runs.id)
+ (SELECT count(*) FROM scenario_runs sr WHERE sr.benchmark_run_id = benchmark_runs.id) as results_count')
->selectRaw('(SELECT count(*) FROM softbench_results sr WHERE sr.benchmark_run_id = benchmark_runs.id AND sr.ok)
+ (SELECT count(*) FROM infrabench_results sr WHERE sr.benchmark_run_id = benchmark_runs.id AND sr.ok)
+ (SELECT count(*) FROM prosebench_results sr WHERE sr.benchmark_run_id = benchmark_runs.id AND sr.ok)
+ (SELECT count(*) FROM psychbench_results sr WHERE sr.benchmark_run_id = benchmark_runs.id AND sr.ok)
+ (SELECT count(*) FROM scenario_runs sr WHERE sr.benchmark_run_id = benchmark_runs.id AND sr.ok) as passed_count')
->when($this->filters['search'], fn ($q, $search) => $q->where(function ($q) use ($search) {
$q->where('run_id', 'like', "%{$search}%")
->orWhere('model_id', 'like', "%{$search}%")
->orWhere('label', 'like', "%{$search}%");
}))
->orderByDesc('ran_at')
->orderByDesc('id')
->paginate(25);
}
};
?>
<div class="flex flex-col gap-6">
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ __('My Runs') }}</flux:heading>
<flux:subheading>{{ __('Runs attributed to you. Toggle visibility or add notes from a run page.') }}</flux:subheading>
</div>
<flux:button variant="ghost" icon="arrow-left" href="{{ route('home') }}" wire:navigate>{{ __('All results') }}</flux:button>
</div>
<flux:input
wire:model.live.debounce.300ms="filters.search"
:label="__('Search')"
:placeholder="__('Run, model or label…')"
size="sm"
class="w-72"
clearable
/>
@if ($this->runs->count() > 0)
<flux:table :paginate="$this->runs">
<flux:table.columns>
<flux:table.column>{{ __('Date') }}</flux:table.column>
<flux:table.column>{{ __('Run') }}</flux:table.column>
<flux:table.column>{{ __('Model') }}</flux:table.column>
<flux:table.column>{{ __('Suite') }}</flux:table.column>
<flux:table.column>{{ __('GPU') }}</flux:table.column>
<flux:table.column class="text-right">{{ __('Tasks') }}</flux:table.column>
<flux:table.column class="text-right">{{ __('Passed') }}</flux:table.column>
<flux:table.column>{{ __('Visibility') }}</flux:table.column>
<flux:table.column></flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach ($this->runs as $run)
<flux:table.row :key="$run->id">
<flux:table.cell>{{ optional($run->ran_at)->format('Y-m-d') }}</flux:table.cell>
<flux:table.cell>
<a href="{{ route('runs.show', $run->run_id) }}" class="font-mono text-xs hover:underline" wire:navigate>{{ $run->run_id }}</a>
</flux:table.cell>
<flux:table.cell>{{ $run->model_id }}</flux:table.cell>
<flux:table.cell><flux:badge size="xs">{{ $run->suite }}</flux:badge></flux:table.cell>
<flux:table.cell>
@if ($run->gpu_id !== null)
<span class="text-xs">{{ $run->gpu }}</span>
@elseif ($run->gpu !== null && ! in_array(strtolower($run->gpu), ['unknown', 'hosted', 'cloud']))
<flux:badge size="xs" color="amber" class="cursor-pointer" wire:click="startGpuConfirm({{ $run->id }})">
{{ __('Unconfirmed') }}: {{ $run->gpu }}
</flux:badge>
@else
<span class="text-xs text-zinc-400">{{ $run->gpu ?? '—' }}</span>
@endif
</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ $run->results_count }}</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ $run->passed_count }}</flux:table.cell>
<flux:table.cell>
<flux:badge size="xs" color="{{ $run->visibility === 'public' ? 'emerald' : 'rose' }}">
{{ __($run->visibility) }}
</flux:badge>
</flux:table.cell>
<flux:table.cell>
<flux:button
size="xs"
variant="ghost"
wire:click="toggleVisibility({{ $run->id }})"
icon="{{ $run->visibility === 'public' ? 'lock-closed' : 'lock-open' }}"
>
{{ $run->visibility === 'public' ? __('Make private') : __('Make public') }}
</flux:button>
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
@else
<flux:callout variant="brand">
<flux:callout.heading>{{ __('No runs yet') }}</flux:callout.heading>
<flux:callout.text>{{ __('Runs you import or submit will appear here.') }}</flux:callout.text>
</flux:callout>
@endif
<flux:modal name="gpu-confirm-modal" class="w-96">
<div class="flex flex-col gap-4">
<div>
<flux:heading size="lg">{{ __('Confirm GPU') }}</flux:heading>
<flux:subheading>{{ __('Pick the card this run executed on. Confirming regroups the run into the matching session.') }}</flux:subheading>
</div>
<flux:input
wire:model.live.debounce.200ms="gpuSearch"
:label="__('Search GPUs')"
:placeholder="__('Type to search the catalog…')"
clearable
/>
<div class="flex max-h-64 flex-col gap-1 overflow-y-auto">
@foreach ($this->gpuChoices as $gpuId => $gpuName)
<flux:button variant="subtle" wire:click="confirmGpu({{ $gpuId }})" class="justify-start">
{{ $gpuName }}
</flux:button>
@endforeach
@if (count($this->gpuChoices) === 0)
<flux:text size="sm" class="text-zinc-400">{{ __('No catalog matches — ask an admin to add this card.') }}</flux:text>
@endif
</div>
</div>
</flux:modal>
</div>