319 lines
22 KiB
PHP
319 lines
22 KiB
PHP
<?php
|
|
|
|
use App\Livewire\IncludesSorting;
|
|
use App\Models\BenchmarkRun;
|
|
use Flux\Flux;
|
|
use Illuminate\Auth\Access\AuthorizationException;
|
|
use Livewire\Attributes\Computed;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Component;
|
|
|
|
new #[Layout('components.layouts.app')] class extends Component {
|
|
use IncludesSorting;
|
|
|
|
private const SORTABLE_SCENARIO = [
|
|
'scenario', 'level', 'ok', 'outcome',
|
|
'wall', 'turns', 'commands', 'submissions', 'rejected',
|
|
't_first_sub', 't_pass', 'gen_tokens',
|
|
];
|
|
|
|
private const SORTABLE_DEPTHSWEEP = [
|
|
'section', 'task', 'level', 'ok', 'ttft', 'wall', 'visible',
|
|
];
|
|
|
|
private const SORTABLE_METRIC = [
|
|
'section', 'task', 'level', 'ok', 'checks_passed',
|
|
'kind', 'user_tps', 'decode', 'ttft', 't_answer',
|
|
'wall', 'reasoning', 'visible', 'finish',
|
|
];
|
|
|
|
public ?string $notes = null;
|
|
|
|
public ?string $runId = null;
|
|
|
|
/** Depth-sweep drill-in filter: only this thinking level's rows. */
|
|
public ?string $level = null;
|
|
|
|
public function mount(string $run): void
|
|
{
|
|
$this->runId = $run;
|
|
$this->level = request()->query('level');
|
|
}
|
|
|
|
#[Computed]
|
|
public function run(): BenchmarkRun
|
|
{
|
|
$run = BenchmarkRun::where('run_id', $this->runId)
|
|
->visibleTo(auth()->user())
|
|
->firstOrFail();
|
|
|
|
if ($run->isOwnedBy(auth()->user()) && $this->notes === null) {
|
|
$this->notes = $run->notes;
|
|
}
|
|
|
|
return $run;
|
|
}
|
|
|
|
public function saveNotes(): void
|
|
{
|
|
$this->authorizeOwner();
|
|
|
|
$this->validate([
|
|
'notes' => ['nullable', 'string', 'max:65535'],
|
|
]);
|
|
|
|
$this->run->fill(['notes' => $this->notes])->save();
|
|
|
|
Flux::toast(variant: 'success', text: __('Notes saved.'));
|
|
}
|
|
|
|
public function toggleVisibility(): void
|
|
{
|
|
$this->authorizeOwner();
|
|
|
|
$this->run->fill([
|
|
'visibility' => $this->run->visibility === BenchmarkRun::VISIBILITY_PUBLIC
|
|
? BenchmarkRun::VISIBILITY_PRIVATE
|
|
: BenchmarkRun::VISIBILITY_PUBLIC,
|
|
])->save();
|
|
|
|
Flux::toast(variant: 'success', text: __('Visibility updated.'));
|
|
}
|
|
|
|
private function authorizeOwner(): void
|
|
{
|
|
if (! $this->run->isOwnedBy(auth()->user())) {
|
|
throw new AuthorizationException;
|
|
}
|
|
}
|
|
|
|
private function sortColumn(array $allowed, string $default = null): string
|
|
{
|
|
if (in_array($this->sortBy, $allowed, true)) {
|
|
return $this->sortBy;
|
|
}
|
|
|
|
return $default ?? $allowed[0];
|
|
}
|
|
|
|
private function sortDir(): string
|
|
{
|
|
return $this->sortDirection === 'asc' ? 'asc' : 'desc';
|
|
}
|
|
};
|
|
?>
|
|
|
|
<div class="flex flex-col gap-6">
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<flux:heading size="xl">{{ $this->run->suite }} <span class="text-zinc-400">·</span> <span class="font-mono text-lg">{{ $this->run->model_id }}</span></flux:heading>
|
|
<flux:subheading>
|
|
@if ($this->run->label)
|
|
<flux:badge color="amber" size="xs">{{ $this->run->label }}</flux:badge>
|
|
@else
|
|
@if ($this->run->model_quant)<flux:badge color="zinc" size="xs">{{ $this->run->model_quant }}</flux:badge> @endif
|
|
@if ($this->run->engine)<flux:badge color="sky" size="xs">{{ $this->run->engine }}</flux:badge> @endif
|
|
@endif
|
|
@if ($this->run->visibility === 'private')<flux:badge color="rose" size="xs">{{ __('Private') }}</flux:badge> @endif
|
|
{{ optional($this->run->ran_at)->format('Y-m-d H:i') }}
|
|
</flux:subheading>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<flux:button variant="ghost" icon="arrow-left" href="{{ route('home') }}" wire:navigate>{{ __('Sessions') }}</flux:button>
|
|
@if ($this->run->isOwnedBy(auth()->user()))
|
|
<flux:button
|
|
size="sm"
|
|
:variant="$this->run->visibility === 'public' ? 'subtle' : 'primary'"
|
|
wire:click="toggleVisibility"
|
|
icon="{{ $this->run->visibility === 'public' ? 'lock-closed' : 'lock-open' }}"
|
|
>
|
|
{{ $this->run->visibility === 'public' ? __('Make private') : __('Make public') }}
|
|
</flux:button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<flux:card>
|
|
<div class="flex flex-wrap items-center gap-x-8 gap-y-2 text-sm">
|
|
<div><span class="text-zinc-500">{{ __('GPU') }}</span><div>{{ ($this->run->gpu_count ? $this->run->gpu_count.'x ' : '').($this->run->gpu ?? 'unknown') }}</div></div>
|
|
<div><span class="text-zinc-500">{{ __('Context') }}</span><div class="font-mono">{{ number_format($this->run->n_ctx ?? 0) }}</div></div>
|
|
<div><span class="text-zinc-500">{{ __('Inference host') }}</span><div><flux:badge size="xs" color="sky">{{ $this->run->engine ?? 'unknown' }}</flux:badge></div></div>
|
|
@if ($this->run->label)
|
|
<div><span class="text-zinc-500">{{ __('Run name') }}</span><div><flux:badge size="xs" color="amber">{{ $this->run->label }}</flux:badge></div></div>
|
|
@endif
|
|
</div>
|
|
@if ($this->run->serverConfig)
|
|
<div class="mt-4">
|
|
<flux:heading size="sm">{{ __('Server config') }} — {{ $this->run->serverConfig->name }}</flux:heading>
|
|
<pre class="mt-2 overflow-x-auto rounded-lg bg-zinc-900 p-4 text-xs leading-relaxed text-zinc-100">{{ $this->run->serverConfig->config_yaml }}</pre>
|
|
</div>
|
|
@endif
|
|
</flux:card>
|
|
|
|
@if ($this->run->isOwnedBy(auth()->user()))
|
|
<flux:card>
|
|
<flux:heading size="md">{{ __('Your notes') }}</flux:heading>
|
|
<flux:subheading>{{ __("Festbench data can't be modified — add your own context here.") }}</flux:subheading>
|
|
<flux:textarea wire:model="notes" rows="4" class="mt-4" placeholder="{{ __('Hardware details, environment, caveats…') }}" />
|
|
<div class="mt-3">
|
|
<flux:button size="sm" variant="primary" wire:click="saveNotes">{{ __('Save notes') }}</flux:button>
|
|
</div>
|
|
</flux:card>
|
|
@endif
|
|
|
|
<div>
|
|
<flux:heading size="md">{{ $this->run->suite === 'scenariobench' ? __('Scenario results') : ($this->run->suite === 'depthsweep' ? __('Depth sweep results') : __('Task results')) }}</flux:heading>
|
|
<div class="mt-3">
|
|
@if ($this->run->suite === 'scenariobench')
|
|
@php($sortCol = $this->sortColumn(self::SORTABLE_SCENARIO))
|
|
<flux:table>
|
|
<flux:table.columns>
|
|
<flux:table.column sortable :sorted="$sortBy === 'scenario'" :direction="$sortDirection" wire:click="sort('scenario')">{{ __('Scenario') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'level'" :direction="$sortDirection" wire:click="sort('level')">{{ __('Level') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'ok'" :direction="$sortDirection" wire:click="sort('ok')">{{ __('Pass') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'outcome'" :direction="$sortDirection" wire:click="sort('outcome')">{{ __('Outcome') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'wall'" :direction="$sortDirection" wire:click="sort('wall')" class="text-center">{{ __('Wall (s)') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'turns'" :direction="$sortDirection" wire:click="sort('turns')" class="text-center">{{ __('Turns') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'commands'" :direction="$sortDirection" wire:click="sort('commands')" class="text-center">{{ __('Commands') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'submissions'" :direction="$sortDirection" wire:click="sort('submissions')" class="text-center">{{ __('Submissions') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'rejected'" :direction="$sortDirection" wire:click="sort('rejected')" class="text-center">{{ __('Rejected') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 't_first_sub'" :direction="$sortDirection" wire:click="sort('t_first_sub')" class="text-center">{{ __('t first sub') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 't_pass'" :direction="$sortDirection" wire:click="sort('t_pass')" class="text-center">{{ __('t pass') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'gen_tokens'" :direction="$sortDirection" wire:click="sort('gen_tokens')" class="text-center">{{ __('Gen tokens') }}</flux:table.column>
|
|
</flux:table.columns>
|
|
|
|
<flux:table.rows>
|
|
@foreach ($this->run->suiteResults($this->level, $sortCol, $this->sortDir()) as $result)
|
|
<flux:table.row :key="$result->id">
|
|
<flux:table.cell>{{ $result->scenario }}</flux:table.cell>
|
|
<flux:table.cell>{{ $result->level ?? '—' }}</flux:table.cell>
|
|
<flux:table.cell>
|
|
@if ($result->ok)
|
|
<flux:icon name="check-circle" variant="solid" class="size-4 text-emerald-500" />
|
|
@elseif ($result->ok === null)
|
|
<span class="text-zinc-400">—</span>
|
|
@else
|
|
<flux:icon name="x-circle" variant="solid" class="size-4 text-red-500" />
|
|
@endif
|
|
</flux:table.cell>
|
|
<flux:table.cell><flux:badge size="xs" color="{{ $result->outcome === 'passed' ? 'emerald' : 'rose' }}">{{ $result->outcome ?? '—' }}</flux:badge></flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->wall, 1) }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->turns) }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->commands) }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->submissions) }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->rejected) }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->t_first_sub, 1) }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ $result->t_pass !== null ? number_format($result->t_pass, 1) : '—' }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->gen_tokens) }}</flux:table.cell>
|
|
</flux:table.row>
|
|
@endforeach
|
|
</flux:table.rows>
|
|
</flux:table>
|
|
@elseif ($this->run->suite === 'depthsweep')
|
|
@if ($this->level)
|
|
<div class="mb-3 flex items-center gap-2">
|
|
<flux:text size="sm">{{ __('Thinking level') }}:</flux:text>
|
|
<flux:badge size="xs" color="sky">{{ $this->level }}</flux:badge>
|
|
<flux:button size="xs" variant="ghost" icon="x-mark" href="{{ route('runs.show', $this->run->run_id) }}" wire:navigate>{{ __('show all levels') }}</flux:button>
|
|
</div>
|
|
@endif
|
|
@php($sortCol = $this->sortColumn(self::SORTABLE_DEPTHSWEEP))
|
|
<flux:table>
|
|
<flux:table.columns>
|
|
<flux:table.column sortable :sorted="$sortBy === 'section'" :direction="$sortDirection" wire:click="sort('section')">{{ __('Depth') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'task'" :direction="$sortDirection" wire:click="sort('task')">{{ __('Task') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'level'" :direction="$sortDirection" wire:click="sort('level')">{{ __('Level') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'ok'" :direction="$sortDirection" wire:click="sort('ok')">{{ __('Found') }}</flux:table.column>
|
|
<flux:table.column>{{ __('Needle') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'ttft'" :direction="$sortDirection" wire:click="sort('ttft')" class="text-center">{{ __('TTFT') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'wall'" :direction="$sortDirection" wire:click="sort('wall')" class="text-center">{{ __('Wall') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'visible'" :direction="$sortDirection" wire:click="sort('visible')" class="text-center">{{ __('Visible') }}</flux:table.column>
|
|
</flux:table.columns>
|
|
|
|
<flux:table.rows>
|
|
@foreach ($this->run->suiteResults($this->level, $sortCol, $this->sortDir()) as $result)
|
|
<flux:table.row :key="$result->id">
|
|
<flux:table.cell><flux:badge size="xs" color="sky">{{ $result->section }}</flux:badge></flux:table.cell>
|
|
<flux:table.cell>{{ $result->task }}</flux:table.cell>
|
|
<flux:table.cell>{{ $result->level ?? '—' }}</flux:table.cell>
|
|
<flux:table.cell>
|
|
@if ($result->ok)
|
|
<flux:icon name="check-circle" variant="solid" class="size-4 text-emerald-500" />
|
|
@else
|
|
<flux:icon name="x-circle" variant="solid" class="size-4 text-red-500" />
|
|
@endif
|
|
</flux:table.cell>
|
|
<flux:table.cell class="font-mono text-xs">{{ $result->detail ?? '—' }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->ttft, 2) }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->wall, 2) }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->visible) }}</flux:table.cell>
|
|
</flux:table.row>
|
|
@endforeach
|
|
</flux:table.rows>
|
|
</flux:table>
|
|
@else
|
|
@php($sortCol = $this->sortColumn(self::SORTABLE_METRIC, $this->run->suite === 'infrabench' ? 'task' : 'section'))
|
|
<flux:table>
|
|
<flux:table.columns>
|
|
@if (in_array($this->run->suite, ['softbench', 'prosebench', 'psychbench']))
|
|
<flux:table.column sortable :sorted="$sortBy === 'section'" :direction="$sortDirection" wire:click="sort('section')">{{ __('Section') }}</flux:table.column>
|
|
@endif
|
|
<flux:table.column sortable :sorted="$sortBy === 'task'" :direction="$sortDirection" wire:click="sort('task')">{{ __('Task') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'level'" :direction="$sortDirection" wire:click="sort('level')">{{ __('Level') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'ok'" :direction="$sortDirection" wire:click="sort('ok')">{{ __('Pass') }}</flux:table.column>
|
|
@if (in_array($this->run->suite, ['prosebench', 'psychbench']))
|
|
<flux:table.column sortable :sorted="$sortBy === 'checks_passed'" :direction="$sortDirection" wire:click="sort('checks_passed')" class="text-center">{{ __('Checks') }}</flux:table.column>
|
|
@endif
|
|
@if ($this->run->suite === 'infrabench')
|
|
<flux:table.column sortable :sorted="$sortBy === 'kind'" :direction="$sortDirection" wire:click="sort('kind')">{{ __('Kind') }}</flux:table.column>
|
|
@endif
|
|
<flux:table.column sortable :sorted="$sortBy === 'user_tps'" :direction="$sortDirection" wire:click="sort('user_tps')" class="text-center">{{ __('User tok/s') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'decode'" :direction="$sortDirection" wire:click="sort('decode')" class="text-center">{{ __('Decode') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'ttft'" :direction="$sortDirection" wire:click="sort('ttft')" class="text-center">{{ __('TTFT') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 't_answer'" :direction="$sortDirection" wire:click="sort('t_answer')" class="text-center">{{ __('t_answer') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'wall'" :direction="$sortDirection" wire:click="sort('wall')" class="text-center">{{ __('Wall') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'reasoning'" :direction="$sortDirection" wire:click="sort('reasoning')" class="text-center">{{ __('Reasoning') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'visible'" :direction="$sortDirection" wire:click="sort('visible')" class="text-center">{{ __('Visible') }}</flux:table.column>
|
|
<flux:table.column sortable :sorted="$sortBy === 'finish'" :direction="$sortDirection" wire:click="sort('finish')">{{ __('Finish') }}</flux:table.column>
|
|
</flux:table.columns>
|
|
|
|
<flux:table.rows>
|
|
@foreach ($this->run->suiteResults(null, $sortCol, $this->sortDir()) as $result)
|
|
<flux:table.row :key="$result->id">
|
|
@if (in_array($this->run->suite, ['softbench', 'prosebench', 'psychbench']))
|
|
<flux:table.cell>{{ $result->section ?? '—' }}</flux:table.cell>
|
|
@endif
|
|
<flux:table.cell>{{ $result->task }}</flux:table.cell>
|
|
<flux:table.cell>{{ $result->level ?? '—' }}</flux:table.cell>
|
|
<flux:table.cell>
|
|
@if ($result->ok)
|
|
<flux:icon name="check-circle" variant="solid" class="size-4 text-emerald-500" />
|
|
@elseif ($result->ok === null)
|
|
<span class="text-zinc-400">—</span>
|
|
@else
|
|
<flux:icon name="x-circle" variant="solid" class="size-4 text-red-500" />
|
|
@endif
|
|
</flux:table.cell>
|
|
@if (in_array($this->run->suite, ['prosebench', 'psychbench']))
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ $result->checks_passed !== null ? $result->checks_passed.'/'.$result->checks_total : '—' }}</flux:table.cell>
|
|
@endif
|
|
@if ($this->run->suite === 'infrabench')
|
|
<flux:table.cell>{{ $result->kind ?? '—' }}</flux:table.cell>
|
|
@endif
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->user_tps, 1) }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->decode, 1) }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->ttft, 2) }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->t_answer, 2) }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->wall, 2) }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->reasoning) }}</flux:table.cell>
|
|
<flux:table.cell class="text-center font-mono text-sm">{{ number_format($result->visible) }}</flux:table.cell>
|
|
<flux:table.cell><flux:badge size="xs" color="{{ $result->finish === 'stop' ? 'emerald' : 'amber' }}">{{ $result->finish ?? '—' }}</flux:badge></flux:table.cell>
|
|
</flux:table.row>
|
|
@endforeach
|
|
</flux:table.rows>
|
|
</flux:table>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|