diff --git a/app/Http/Controllers/Auth/VerifyEmailController.php b/app/Http/Controllers/Auth/VerifyEmailController.php index bfeef5a..d263c0c 100644 --- a/app/Http/Controllers/Auth/VerifyEmailController.php +++ b/app/Http/Controllers/Auth/VerifyEmailController.php @@ -16,7 +16,7 @@ class VerifyEmailController extends Controller public function __invoke(EmailVerificationRequest $request): RedirectResponse { if ($request->user()->hasVerifiedEmail()) { - return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + return redirect()->intended(route('home', absolute: false).'?verified=1'); } if ($request->user()->markEmailAsVerified()) { @@ -26,6 +26,6 @@ public function __invoke(EmailVerificationRequest $request): RedirectResponse event(new Verified($user)); } - return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + return redirect()->intended(route('home', absolute: false).'?verified=1'); } } diff --git a/app/Livewire/IncludesSorting.php b/app/Livewire/IncludesSorting.php new file mode 100755 index 0000000..c708dcf --- /dev/null +++ b/app/Livewire/IncludesSorting.php @@ -0,0 +1,20 @@ +sortBy === $column) { + $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + $this->sortBy = $column; + $this->sortDirection = 'desc'; + } + } +} diff --git a/app/Models/BenchmarkRun.php b/app/Models/BenchmarkRun.php index bbb6165..94e25e2 100644 --- a/app/Models/BenchmarkRun.php +++ b/app/Models/BenchmarkRun.php @@ -84,7 +84,19 @@ public function isOwnedBy(?User $user): bool */ public function scopeVisibleTo(Builder $query, ?User $user): Builder { - return $query->where(function (Builder $q) use ($user) { + return $query->where(self::visibleClause($user)); + } + + /** + * The visibility predicate as a standalone closure, so joined queries + * (e.g. the flat results table) can apply the exact same rule without + * duplicating the logic. + * + * @return \Closure(Builder): Builder + */ + public static function visibleClause(?User $user): \Closure + { + return function (Builder $q) use ($user) { $q->where('visibility', self::VISIBILITY_PUBLIC); if ($user !== null) { @@ -92,6 +104,6 @@ public function scopeVisibleTo(Builder $query, ?User $user): Builder ->where('visibility', self::VISIBILITY_PRIVATE) ->where('user_id', $user->id)); } - }); + }; } } diff --git a/resources/views/components/layouts/app/header.blade.php b/resources/views/components/layouts/app/header.blade.php index 4bce1ec..75298c2 100644 --- a/resources/views/components/layouts/app/header.blade.php +++ b/resources/views/components/layouts/app/header.blade.php @@ -7,12 +7,12 @@ - + - + Dashboard @@ -44,10 +44,13 @@ class="h-10 max-lg:hidden [&>div>svg]:size-5" - + @guest + {{ __('Log in') }} + @else + @@ -58,13 +61,13 @@ class="cursor-pointer" - {{ auth()->user()->initials() }} + {{ auth()->user()?->initials() ?? '' }}
- {{ auth()->user()->name }} - {{ auth()->user()->email }} + {{ auth()->user()?->name ?? '' }} + {{ auth()->user()?->email ?? '' }}
@@ -86,19 +89,20 @@ class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200
+@endguest
- + - + Dashboard diff --git a/resources/views/components/layouts/app/sidebar.blade.php b/resources/views/components/layouts/app/sidebar.blade.php index 6cebd79..25d6fc2 100644 --- a/resources/views/components/layouts/app/sidebar.blade.php +++ b/resources/views/components/layouts/app/sidebar.blade.php @@ -7,13 +7,17 @@ - + - Dashboard + Results + @auth + My Runs + Dashboard + @endauth @@ -30,10 +34,13 @@ - + @guest + {{ __('Log in') }} + @else + @@ -45,13 +52,13 @@ - {{ auth()->user()->initials() }} + {{ auth()->user()?->initials() ?? '' }}
- {{ auth()->user()->name }} - {{ auth()->user()->email }} + {{ auth()->user()?->name ?? '' }} + {{ auth()->user()?->email ?? '' }}
@@ -73,6 +80,7 @@ class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200
+@endguest
@@ -81,9 +89,12 @@ class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 - + @guest + {{ __('Log in') }} + @else + @@ -95,13 +106,13 @@ class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 - {{ auth()->user()->initials() }} + {{ auth()->user()?->initials() ?? '' }}
- {{ auth()->user()->name }} - {{ auth()->user()->email }} + {{ auth()->user()?->name ?? '' }} + {{ auth()->user()?->email ?? '' }}
@@ -123,6 +134,7 @@ class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200
+@endguest {{ $slot }} diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php deleted file mode 100644 index d594e8a..0000000 --- a/resources/views/dashboard.blade.php +++ /dev/null @@ -1,18 +0,0 @@ - -
-
-
- -
-
- -
-
- -
-
-
- -
-
-
diff --git a/resources/views/livewire/auth/confirm-password.blade.php b/resources/views/livewire/auth/confirm-password.blade.php index 62a86d8..5aaee07 100644 --- a/resources/views/livewire/auth/confirm-password.blade.php +++ b/resources/views/livewire/auth/confirm-password.blade.php @@ -28,7 +28,7 @@ public function confirmPassword(): void session(['auth.password_confirmed_at' => time()]); - $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); + $this->redirectIntended(default: route('home', absolute: false), navigate: true); } }; ?> diff --git a/resources/views/livewire/auth/login.blade.php b/resources/views/livewire/auth/login.blade.php index 2a0387b..318fa3a 100644 --- a/resources/views/livewire/auth/login.blade.php +++ b/resources/views/livewire/auth/login.blade.php @@ -39,7 +39,7 @@ public function login(): void RateLimiter::clear($this->throttleKey()); Session::regenerate(); - $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); + $this->redirectIntended(default: route('home', absolute: false), navigate: true); } /** diff --git a/resources/views/livewire/auth/register.blade.php b/resources/views/livewire/auth/register.blade.php index 59964a5..da5a33e 100644 --- a/resources/views/livewire/auth/register.blade.php +++ b/resources/views/livewire/auth/register.blade.php @@ -31,7 +31,7 @@ public function register(): void Auth::login($user); - $this->redirect(route('dashboard', absolute: false), navigate: true); + $this->redirect(route('home', absolute: false), navigate: true); } }; ?> diff --git a/resources/views/livewire/auth/verify-email.blade.php b/resources/views/livewire/auth/verify-email.blade.php index f1ad9e0..04f595c 100644 --- a/resources/views/livewire/auth/verify-email.blade.php +++ b/resources/views/livewire/auth/verify-email.blade.php @@ -13,7 +13,7 @@ public function sendVerification(): void { if (Auth::user()->hasVerifiedEmail()) { - $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); + $this->redirectIntended(default: route('home', absolute: false), navigate: true); return; } diff --git a/resources/views/livewire/my-runs/index.blade.php b/resources/views/livewire/my-runs/index.blade.php new file mode 100644 index 0000000..ee8b9a9 --- /dev/null +++ b/resources/views/livewire/my-runs/index.blade.php @@ -0,0 +1,125 @@ + 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.')); + } + + public function resetFilters(): void + { + $this->filters = ['search' => null]; + $this->resetPage(); + } + + #[Computed] + public function runs(): LengthAwarePaginator + { + return BenchmarkRun::query() + ->where('user_id', auth()->id()) + ->withCount([ + 'results', + 'results as passed_count' => fn ($q) => $q->where('ok', true), + ]) + ->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); + } +}; +?> + +
+
+
+ {{ __('My Runs') }} + {{ __('Runs attributed to you. Toggle visibility or add notes from a run page.') }} +
+ {{ __('All results') }} +
+ + + + @if ($this->runs->count() > 0) + + + {{ __('Date') }} + {{ __('Run') }} + {{ __('Model') }} + {{ __('Suite') }} + {{ __('Tasks') }} + {{ __('Passed') }} + {{ __('Visibility') }} + + + + + @foreach ($this->runs as $run) + + {{ optional($run->ran_at)->format('Y-m-d') }} + + {{ $run->run_id }} + + {{ $run->model_id }} + {{ $run->suite }} + {{ $run->results_count }} + {{ $run->passed_count }} + + + {{ __($run->visibility) }} + + + + + {{ $run->visibility === 'public' ? __('Make private') : __('Make public') }} + + + + @endforeach + + + @else + + {{ __('No runs yet') }} + {{ __('Runs you import or submit will appear here.') }} + + @endif +
\ No newline at end of file diff --git a/resources/views/livewire/results/index.blade.php b/resources/views/livewire/results/index.blade.php new file mode 100644 index 0000000..f3a84e4 --- /dev/null +++ b/resources/views/livewire/results/index.blade.php @@ -0,0 +1,203 @@ + null, + 'suite' => null, + 'level' => null, + 'model' => null, + 'result' => null, + ]; + + private const SORTABLE = [ + 'ran_at', 'model_id', 'suite', 'task', 'section', 'level', 'ok', + 'decode', 'user_tps', 'ttft', 't_answer', 'wall', 'total', + ]; + + public function resetFilters(): void + { + $this->filters = [ + 'search' => null, + 'suite' => null, + 'level' => null, + 'model' => null, + 'result' => null, + ]; + + $this->resetPage(); + } + + #[Computed] + public function results(): LengthAwarePaginator + { + return RunResult::query() + ->join('benchmark_runs', 'run_results.benchmark_run_id', '=', 'benchmark_runs.id') + ->select('run_results.*', + 'benchmark_runs.ran_at', + 'benchmark_runs.model_id', + 'benchmark_runs.model_quant', + 'benchmark_runs.engine', + 'benchmark_runs.run_id', + 'benchmark_runs.visibility', + 'benchmark_runs.user_id as run_user_id') + ->tap(fn (Builder $q) => $q->where(BenchmarkRun::visibleClause(auth()->user()))) + ->tap(fn (Builder $q) => $this->applySorting($q)) + ->tap(fn (Builder $q) => $this->applyFilters($q)) + ->with('run:id,run_id') + ->paginate(25); + } + + #[Computed] + public function suites(): array + { + return RunResult::query()->distinct()->orderBy('suite')->pluck('suite')->all(); + } + + #[Computed] + public function models(): array + { + return BenchmarkRun::visibleTo(auth()->user()) + ->distinct()->orderBy('model_id')->pluck('model_id')->all(); + } + + private function applySorting(Builder $query): Builder + { + $column = in_array($this->sortBy, self::SORTABLE, true) ? $this->sortBy : 'ran_at'; + + return $query->orderBy($column, $this->sortDirection === 'asc' ? 'asc' : 'desc') + ->orderBy('run_results.id', 'desc'); + } + + private function applyFilters(Builder $query): Builder + { + return $query + ->when($this->filters['search'], fn (Builder $q, string $search) => $q->where(function (Builder $q) use ($search) { + $q->where('run_results.task', 'like', "%{$search}%") + ->orWhere('benchmark_runs.model_id', 'like', "%{$search}%") + ->orWhere('benchmark_runs.label', 'like', "%{$search}%"); + })) + ->when($this->filters['suite'], fn (Builder $q, string $suite) => $q->where('run_results.suite', $suite)) + ->when($this->filters['level'], fn (Builder $q, string $level) => $q->where('run_results.level', $level)) + ->when($this->filters['model'], fn (Builder $q, string $model) => $q->where('benchmark_runs.model_id', $model)) + ->when($this->filters['result'] !== null && $this->filters['result'] !== '', fn (Builder $q) => $q->where('run_results.ok', (bool) $this->filters['result'])); + } +}; +?> + +
+
+
+ {{ __('Benchmark Results') }} + {{ __('Every datapoint festbench captures — sortable.') }} +
+
+ +
+ + + + @foreach ($this->suites as $suite) + {{ $suite }} + @endforeach + + + + @foreach (['off', 'low', 'medium', 'xhigh'] as $level) + {{ $level }} + @endforeach + + + + @foreach ($this->models as $model) + {{ $model }} + @endforeach + + + + {{ __('Pass') }} + {{ __('Fail') }} + + + {{ __('Clear') }} +
+ + @if ($this->results->count() > 0) +
+ + + {{ __('Date') }} + {{ __('Model') }} + {{ __('Suite') }} + {{ __('Task') }} + {{ __('Level') }} + {{ __('Pass') }} + {{ __('User tok/s') }} + {{ __('Decode') }} + {{ __('TTFT') }} + {{ __('Wall') }} + {{ __('Tokens') }} + + + + @foreach ($this->results as $result) + + {{ optional($result->ran_at)->format('Y-m-d') }} + + + {{ $result->model_id }} + @if ($result->model_quant) + {{ $result->model_quant }} + @endif + + + {{ $result->suite }} + {{ $result->task }} + {{ $result->level ?? '—' }} + + @if ($result->ok) + + @elseif ($result->ok === null) + — + @else + + @endif + + {{ number_format($result->user_tps, 1) }} + {{ number_format($result->decode, 1) }} + {{ number_format($result->ttft, 2) }} + {{ number_format($result->wall, 2) }} + {{ number_format($result->total) }} + + @endforeach + + +
+ @else +
+ + {{ __('No results') }} + {{ __('No benchmark results match the current filters.') }} + +
+ @endif +
\ No newline at end of file diff --git a/resources/views/livewire/runs/show.blade.php b/resources/views/livewire/runs/show.blade.php new file mode 100644 index 0000000..8a9580b --- /dev/null +++ b/resources/views/livewire/runs/show.blade.php @@ -0,0 +1,173 @@ +runId = $run; + } + + #[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; + } + } +}; +?> + +
+
+
+ {{ $this->run->suite }} · {{ $this->run->model_id }} + + @if ($this->run->model_quant){{ $this->run->model_quant }} @endif + @if ($this->run->engine){{ $this->run->engine }} @endif + @if ($this->run->label){{ $this->run->label }} @endif + @if ($this->run->visibility === 'private'){{ __('Private') }} @endif + {{ optional($this->run->ran_at)->format('Y-m-d H:i') }} + +
+ +
+ {{ __('Back') }} + @if ($this->run->isOwnedBy(auth()->user())) + + {{ $this->run->visibility === 'public' ? __('Make private') : __('Make public') }} + + @endif +
+
+ + + {{ __('Run metadata') }} +
+
{{ __('Engine build') }}
{{ $this->run->engine_build ?? '—' }}
+
{{ __('Context') }}
{{ number_format($this->run->n_ctx ?? 0) }}
+
{{ __('Slots') }}
{{ $this->run->slots ?? '—' }}
+
{{ __('GPU') }}
{{ ($this->run->gpu_count ? $this->run->gpu_count.'x ' : '').($this->run->gpu ?? 'unknown') }}
+
{{ __('Image digest') }}
{{ $this->run->image_digest ?? '—' }}
+ @if ($this->run->suite_versions) +
{{ __('Suite versions') }}
{{ json_encode($this->run->suite_versions) }}
+ @endif + @if ($this->run->validators) +
{{ __('Validators') }}
{{ json_encode($this->run->validators) }}
+ @endif +
+
+ + @if ($this->run->isOwnedBy(auth()->user())) + + {{ __('Your notes') }} + {{ __("Festbench data can't be modified — add your own context here.") }} + +
+ {{ __('Save notes') }} +
+
+ @endif + +
+ {{ __('Task results') }} +
+ + + {{ __('Section') }} + {{ __('Task') }} + {{ __('Level') }} + {{ __('Pass') }} + {{ __('User tok/s') }} + {{ __('Decode') }} + {{ __('TTFT') }} + {{ __('t_answer') }} + {{ __('Wall') }} + {{ __('Reasoning') }} + {{ __('Visible') }} + {{ __('Finish') }} + + + + @foreach ($this->run->results()->orderBy('section')->orderBy('task')->get() as $result) + + {{ $result->section ?? '—' }} + {{ $result->task }} + {{ $result->level ?? '—' }} + + @if ($result->ok) + + @elseif ($result->ok === null) + — + @else + + @endif + + {{ number_format($result->user_tps, 1) }} + {{ number_format($result->decode, 1) }} + {{ number_format($result->ttft, 2) }} + {{ number_format($result->t_answer, 2) }} + {{ number_format($result->wall, 2) }} + {{ number_format($result->reasoning) }} + {{ number_format($result->visible) }} + {{ $result->finish ?? '—' }} + + @endforeach + + +
+
+
\ No newline at end of file diff --git a/resources/views/livewire/settings/profile.blade.php b/resources/views/livewire/settings/profile.blade.php index f1b07c3..5b7e481 100644 --- a/resources/views/livewire/settings/profile.blade.php +++ b/resources/views/livewire/settings/profile.blade.php @@ -58,7 +58,7 @@ public function resendVerificationNotification(): void $user = Auth::user(); if ($user->hasVerifiedEmail()) { - $this->redirectIntended(default: route('dashboard', absolute: false)); + $this->redirectIntended(default: route('home', absolute: false)); return; } diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php index e054d63..cbfd878 100644 --- a/resources/views/welcome.blade.php +++ b/resources/views/welcome.blade.php @@ -21,7 +21,7 @@