Compare commits

...

27 Commits

Author SHA1 Message Date
Ninluc 21c02706de Dark mode by default
linter / quality (push) Successful in 4m25s
tests / ci (8.3) (push) Successful in 4m17s
2026-09-19 18:04:29 +02:00
Ninluc 7c223e0ba8 Increase max request time
linter / quality (push) Successful in 5m1s
tests / ci (8.3) (push) Successful in 3m47s
2026-09-19 15:28:52 +02:00
Ninluc 6d8e3438f0 Track some events
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Successful in 3m57s
2026-09-19 10:43:05 +02:00
Ninluc ae676343a5 Fix send cancel request
linter / quality (push) Successful in 5m1s
tests / ci (8.3) (push) Successful in 4m28s
2026-09-18 23:58:51 +02:00
Ninluc 9389aef632 Fix cancel route
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Successful in 3m49s
2026-09-18 23:41:31 +02:00
Ninluc fa6c427d76 Fix cancel ?
linter / quality (push) Failing after 1m3s
tests / ci (8.3) (push) Successful in 3m48s
2026-09-18 23:24:13 +02:00
Ninluc 25b03fc39a Cancel Training
linter / quality (push) Failing after 1m11s
tests / ci (8.3) (push) Successful in 3m55s
2026-09-18 23:08:19 +02:00
Ninluc d80bfe3cdd Updated Interface Image and added excalidraws
linter / quality (push) Successful in 6m26s
tests / ci (8.3) (push) Successful in 5m11s
2026-09-18 14:19:17 +02:00
Ninluc 2332e805e8 Logarithmic error scale
linter / quality (push) Successful in 5m12s
tests / ci (8.3) (push) Successful in 4m29s
2026-09-18 14:12:23 +02:00
Ninluc 4610334f72 Do not exclude get parameters
linter / quality (push) Successful in 4m54s
tests / ci (8.3) (push) Successful in 4m2s
2026-09-18 09:03:46 +02:00
Ninluc 9ae3a6942b Default perrceptron to simple
linter / quality (push) Successful in 4m36s
tests / ci (8.3) (push) Successful in 3m48s
2026-09-17 21:55:58 +02:00
Ninluc 3a19691591 Remove debug config value
linter / quality (push) Successful in 4m11s
tests / ci (8.3) (push) Successful in 3m39s
2026-09-17 21:46:49 +02:00
Ninluc 2d7c1873bb Fix Bar chart
linter / quality (push) Successful in 5m36s
tests / ci (8.3) (push) Successful in 4m42s
2026-09-17 21:24:42 +02:00
Ninluc 5d68757e9d Fix Reverb ?
linter / quality (push) Successful in 6m20s
tests / ci (8.3) (push) Successful in 4m57s
2026-09-17 21:01:15 +02:00
Ninluc 2de27af03b Some anonymous analytics I promise 2026-09-17 20:58:06 +02:00
Ninluc be7b276dec Remove asset building for test 2026-09-17 19:56:38 +02:00
Ninluc 2345b850b8 ix scatter graph
linter / quality (push) Successful in 5m52s
tests / ci (8.3) (push) Successful in 6m19s
2026-09-17 19:45:02 +02:00
Ninluc 538ddc9679 Fix reverb
linter / quality (push) Successful in 6m42s
tests / ci (8.3) (push) Successful in 5m46s
2026-09-17 18:58:54 +02:00
Ninluc 099318c79e USE secrets
linter / quality (push) Successful in 4m10s
tests / ci (8.3) (push) Successful in 4m21s
2026-09-17 09:30:50 +02:00
Ninluc 1517920fce Use env var
linter / quality (push) Successful in 4m46s
tests / ci (8.3) (push) Successful in 4m23s
2026-09-16 23:06:34 +02:00
Ninluc d4f98517cd misc
linter / quality (push) Successful in 4m27s
tests / ci (8.3) (push) Successful in 4m9s
2026-09-16 22:41:11 +02:00
Ninluc feac0feaeb Trust proxies 2026-09-16 22:41:03 +02:00
Ninluc ba4b951a69 Revert "Migrate database"
linter / quality (push) Successful in 5m8s
tests / ci (8.3) (push) Successful in 5m49s
This reverts commit 41b0e8a64a.
2026-09-16 21:58:12 +02:00
Ninluc 41b0e8a64a Migrate database
linter / quality (push) Failing after 2m48s
tests / ci (8.3) (push) Successful in 4m38s
2026-09-16 21:34:38 +02:00
Ninluc da53bf16e9 Fix repository name
linter / quality (push) Successful in 4m11s
tests / ci (8.3) (push) Successful in 4m12s
2026-09-16 20:50:39 +02:00
Ninluc 3ad2e7d999 Fix tests versions 2026-09-16 20:47:33 +02:00
Ninluc 00f379811f Renammed project 2026-09-16 20:35:08 +02:00
36 changed files with 9740 additions and 355 deletions
+8
View File
@@ -3,6 +3,7 @@ APP_ENV=production
APP_KEY=base64:WE5DlJGDeIrl+B1ZqahVJSg4TN6oY3N1ph8CDKNo0w4= APP_KEY=base64:WE5DlJGDeIrl+B1ZqahVJSg4TN6oY3N1ph8CDKNo0w4=
APP_DEBUG=false APP_DEBUG=false
APP_URL=https://perceptron.matthiasg.dev APP_URL=https://perceptron.matthiasg.dev
ASSET_URL=https://perceptron.matthiasg.dev
APP_LOCALE=fr APP_LOCALE=fr
APP_FALLBACK_LOCALE=fr APP_FALLBACK_LOCALE=fr
@@ -11,6 +12,10 @@ APP_FAKER_LOCALE=fr_FR
APP_MAINTENANCE_DRIVER=file APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database # APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=single LOG_CHANNEL=single
LOG_STACK=single LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
@@ -40,6 +45,9 @@ REVERB_APP_SECRET=oosuq0v9jgaslzp9cmhv
REVERB_HOST="perceptron.matthiasg.dev" REVERB_HOST="perceptron.matthiasg.dev"
REVERB_PORT=443 REVERB_PORT=443
REVERB_SCHEME=https REVERB_SCHEME=https
REVERB_BROADCAST_HOST=reverb
REVERB_BROADCAST_PORT=8080
REVERB_BROADCAST_SCHEME=http
REVERB_MAX_REQUEST_SIZE=100000 REVERB_MAX_REQUEST_SIZE=100000
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
+8 -1
View File
@@ -14,6 +14,8 @@ jobs:
packages: write packages: write
env: env:
RUNNER_TOOL_CACHE: /toolcache RUNNER_TOOL_CACHE: /toolcache
REVERB_APP_KEY: ${{ secrets.REVERB_APP_KEY }}
REVERB_APP_SECRET: ${{ secrets.REVERB_APP_SECRET }}
steps: steps:
- name: Check out repository code - name: Check out repository code
@@ -47,7 +49,12 @@ jobs:
context: . context: .
file: Dockerfile file: Dockerfile
push: true push: true
tags: git.matthiasg.dev/Ninluc/perceptron-viewer/perceptron-viewer:latest, git.matthiasg.dev/Ninluc/perceptron-viewer/perceptron-viewer:${{ steps.get_version.outputs.version }} build-args: |
VITE_REVERB_APP_KEY=${{ secrets.REVERB_APP_KEY }}
VITE_REVERB_HOST=perceptron.matthiasg.dev
VITE_REVERB_PORT=443
VITE_REVERB_SCHEME=https
tags: git.matthiasg.dev/ninluc/perceptron-viewer/perceptron-viewer:latest, git.matthiasg.dev/ninluc/perceptron-viewer/perceptron-viewer:${{ steps.get_version.outputs.version }}
# notify: # notify:
# needs: build # needs: build
+3 -3
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
php-version: ['8.4', '8.5'] php-version: ['8.3']
steps: steps:
- name: Checkout code - name: Checkout code
@@ -80,8 +80,8 @@ jobs:
# ------------------------- # -------------------------
# Build (optional remove if not needed for tests) # Build (optional remove if not needed for tests)
# ------------------------- # -------------------------
- name: Build Assets # - name: Build Assets
run: npm run build # run: npm run build
# ------------------------- # -------------------------
# Run tests (parallel) # Run tests (parallel)
+12
View File
@@ -9,11 +9,23 @@ RUN composer install --no-dev --optimize-autoloader --no-interaction
# Install node and npm # Install node and npm
RUN apk add --no-cache nodejs npm RUN apk add --no-cache nodejs npm
RUN npm install RUN npm install
ARG VITE_REVERB_APP_KEY
ARG VITE_REVERB_HOST
ARG VITE_REVERB_PORT=443
ARG VITE_REVERB_SCHEME=https
ENV VITE_REVERB_APP_KEY=$VITE_REVERB_APP_KEY \
VITE_REVERB_HOST=$VITE_REVERB_HOST \
VITE_REVERB_PORT=$VITE_REVERB_PORT \
VITE_REVERB_SCHEME=$VITE_REVERB_SCHEME
RUN npm run build RUN npm run build
# Final PHP image # Final PHP image
FROM dunglas/frankenphp:1.12.2-php8.3-alpine AS final FROM dunglas/frankenphp:1.12.2-php8.3-alpine AS final
COPY docker/php.ini-production "$PHP_INI_DIR/php.ini"
# Install system dependencies # Install system dependencies
RUN apk add --no-cache \ RUN apk add --no-cache \
bash \ bash \
+1 -1
View File
@@ -1,4 +1,4 @@
# Perceptron and neuronal networks # PerceptronViewer
Using Laravel and Vue JS Using Laravel and Vue JS
@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions;
use RuntimeException;
class TrainingCancelledException extends RuntimeException
{
}
+30 -6
View File
@@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Events\PerceptronInitialization; use App\Events\PerceptronInitialization;
use App\Exceptions\TrainingCancelledException;
use App\Http\Requests\RunPerceptronRequest; use App\Http\Requests\RunPerceptronRequest;
use App\Models\NetworksTraining\ADALINEPerceptronTraining; use App\Models\NetworksTraining\ADALINEPerceptronTraining;
use App\Models\NetworksTraining\GradientDescentPerceptronTraining; use App\Models\NetworksTraining\GradientDescentPerceptronTraining;
@@ -18,15 +19,31 @@ use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use App\Services\SynapticWeightsProvider\RandomSynapticWeights; use App\Services\SynapticWeightsProvider\RandomSynapticWeights;
use App\Services\SynapticWeightsProvider\ZeroSynapticWeights; use App\Services\SynapticWeightsProvider\ZeroSynapticWeights;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class PerceptronController extends Controller class PerceptronController extends Controller
{ {
private function cancellationKey(string $trainingId): string
{
return "perceptron-training-cancelled:{$trainingId}";
}
public function cancel(Request $request)
{
$trainingId = $request->validate([
'training_id' => ['required', 'string', 'max:100'],
])['training_id'];
Cache::put($this->cancellationKey($trainingId), true, now()->addHour());
return response()->noContent();
}
/** /**
* Display the specified resource. * Display the specified resource.
*/ */
public function index(Request $request) public function index(Request $request)
{ {
$perceptronType = $request->query('type'); $perceptronType = $request->query('type', 'simple'); ;
$learningRate = 0.01; $learningRate = 0.01;
$maxIterations = 200; $maxIterations = 200;
@@ -220,6 +237,8 @@ class PerceptronController extends Controller
$sessionId = $request->input('session_id', session()->getId()); $sessionId = $request->input('session_id', session()->getId());
$trainingId = $request->input('training_id'); $trainingId = $request->input('training_id');
Cache::forget($this->cancellationKey($trainingId));
// Zero initialization prevents hidden layers from receiving a gradient. // Zero initialization prevents hidden layers from receiving a gradient.
if ($perceptronType === 'multilayer' && $weightInitMethod === 'zeros') { if ($perceptronType === 'multilayer' && $weightInitMethod === 'zeros') {
$synapticWeightsProvider = new RandomSynapticWeights; $synapticWeightsProvider = new RandomSynapticWeights;
@@ -236,17 +255,22 @@ class PerceptronController extends Controller
$datasetReader = $this->getDataSetReader($dataSet); $datasetReader = $this->getDataSetReader($dataSet);
$networkTraining = match ($perceptronType) { $networkTraining = match ($perceptronType) {
'simple' => new SimpleBinaryPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId), 'simple' => new SimpleBinaryPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
'gradientdescent' => new GradientDescentPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError), 'gradientdescent' => new GradientDescentPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
'adaline' => new ADALINEPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError), 'adaline' => new ADALINEPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
'monolayer' => new MonoLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError), 'monolayer' => new MonoLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
'multilayer' => new MultiLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $hiddenLayers, $hiddenLayersNeurons, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError), 'multilayer' => new MultiLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $hiddenLayers, $hiddenLayersNeurons, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError, fn (): bool => connection_aborted() || Cache::has($this->cancellationKey($trainingId))),
default => null, default => null,
}; };
event(new PerceptronInitialization($datasetReader->lines, $networkTraining->activationFunction, $sessionId, $trainingId)); event(new PerceptronInitialization($datasetReader->lines, $networkTraining->activationFunction, $sessionId, $trainingId));
try {
$networkTraining->start(); $networkTraining->start();
} catch (TrainingCancelledException) {
$networkTraining->cancel();
Cache::forget($this->cancellationKey($trainingId));
}
return back()->with('success', [ return back()->with('success', [
'message' => 'Training completed', 'message' => 'Training completed',
@@ -8,6 +8,7 @@ use App\Models\Perceptrons\Perceptron;
use App\Services\DatasetReader\IDataSetReader; use App\Services\DatasetReader\IDataSetReader;
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer; use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider; use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use Closure;
class ADALINEPerceptronTraining extends NetworkTraining class ADALINEPerceptronTraining extends NetworkTraining
{ {
@@ -26,8 +27,9 @@ class ADALINEPerceptronTraining extends NetworkTraining
string $sessionId, string $sessionId,
string $trainingId, string $trainingId,
private float $minError, private float $minError,
?Closure $isCancelled = null,
) { ) {
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId); parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
$this->perceptron = new GradientDescentPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize())); $this->perceptron = new GradientDescentPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize()));
} }
@@ -8,6 +8,7 @@ use App\Models\Perceptrons\Perceptron;
use App\Services\DatasetReader\IDataSetReader; use App\Services\DatasetReader\IDataSetReader;
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer; use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider; use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use Closure;
class GradientDescentPerceptronTraining extends NetworkTraining class GradientDescentPerceptronTraining extends NetworkTraining
{ {
@@ -26,8 +27,9 @@ class GradientDescentPerceptronTraining extends NetworkTraining
string $sessionId, string $sessionId,
string $trainingId, string $trainingId,
private float $minError, private float $minError,
?Closure $isCancelled = null,
) { ) {
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId); parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
$this->perceptron = new GradientDescentPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize())); $this->perceptron = new GradientDescentPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize()));
} }
@@ -10,6 +10,7 @@ use App\Services\DatasetReader\IDataSetReader;
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer; use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider; use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use App\Services\SynapticWeightsProvider\SimpleNetworkWeightsProvider; use App\Services\SynapticWeightsProvider\SimpleNetworkWeightsProvider;
use Closure;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
class MonoLayerPerceptronTraining extends NetworkTraining class MonoLayerPerceptronTraining extends NetworkTraining
@@ -35,8 +36,9 @@ class MonoLayerPerceptronTraining extends NetworkTraining
string $sessionId, string $sessionId,
string $trainingId, string $trainingId,
private float $minError, private float $minError,
?Closure $isCancelled = null,
) { ) {
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId); parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
$this->isRegression = $datasetReader->getInputSize() === 1; $this->isRegression = $datasetReader->getInputSize() === 1;
$networkWeightsProvider = new SimpleNetworkWeightsProvider($synapticWeightsProvider); $networkWeightsProvider = new SimpleNetworkWeightsProvider($synapticWeightsProvider);
$this->network = new NetworkPerceptron( $this->network = new NetworkPerceptron(
@@ -11,6 +11,7 @@ use App\Services\DatasetReader\IDataSetReader;
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer; use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider; use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use App\Services\SynapticWeightsProvider\SimpleNetworkWeightsProvider; use App\Services\SynapticWeightsProvider\SimpleNetworkWeightsProvider;
use Closure;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
class MultiLayerPerceptronTraining extends NetworkTraining class MultiLayerPerceptronTraining extends NetworkTraining
@@ -37,8 +38,9 @@ class MultiLayerPerceptronTraining extends NetworkTraining
string $sessionId, string $sessionId,
string $trainingId, string $trainingId,
private float $minError, private float $minError,
?Closure $isCancelled = null,
) { ) {
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId); parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
$this->labels = $datasetReader->getLabels(); $this->labels = $datasetReader->getLabels();
$this->isRegression = $datasetReader->getOutputSize() === 1 $this->isRegression = $datasetReader->getOutputSize() === 1
|| ($datasetReader->getOutputSize() > 2 || ($datasetReader->getOutputSize() > 2
@@ -3,9 +3,11 @@
namespace App\Models\NetworksTraining; namespace App\Models\NetworksTraining;
use App\Events\PerceptronTrainingEnded; use App\Events\PerceptronTrainingEnded;
use App\Exceptions\TrainingCancelledException;
use App\Models\ActivationsFunctions; use App\Models\ActivationsFunctions;
use App\Services\DatasetReader\IDataSetReader; use App\Services\DatasetReader\IDataSetReader;
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer; use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
use Closure;
abstract class NetworkTraining abstract class NetworkTraining
{ {
@@ -24,6 +26,7 @@ abstract class NetworkTraining
protected IPerceptronIterationEventBuffer $iterationEventBuffer, protected IPerceptronIterationEventBuffer $iterationEventBuffer,
protected string $sessionId, protected string $sessionId,
protected string $trainingId, protected string $trainingId,
protected ?Closure $isCancelled = null,
) {} ) {}
abstract public function start(): void; abstract public function start(): void;
@@ -50,9 +53,18 @@ abstract class NetworkTraining
protected function addIterationToBuffer(float $error, array $synapticWeights) protected function addIterationToBuffer(float $error, array $synapticWeights)
{ {
if ($this->isCancelled !== null && ($this->isCancelled)()) {
throw new TrainingCancelledException;
}
$this->iterationEventBuffer->addIteration($this->epoch, $this->datasetReader->getLastReadLineIndex(), $error, $synapticWeights); $this->iterationEventBuffer->addIteration($this->epoch, $this->datasetReader->getLastReadLineIndex(), $error, $synapticWeights);
} }
public function cancel(): void
{
$this->broadcastTrainingEnded('Entraînement annulé');
}
public function getEpoch(): int public function getEpoch(): int
{ {
return $this->epoch; return $this->epoch;
@@ -9,6 +9,7 @@ use App\Models\Perceptrons\SimpleBinaryPerceptron;
use App\Services\DatasetReader\IDataSetReader; use App\Services\DatasetReader\IDataSetReader;
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer; use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider; use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use Closure;
class SimpleBinaryPerceptronTraining extends NetworkTraining class SimpleBinaryPerceptronTraining extends NetworkTraining
{ {
@@ -28,8 +29,9 @@ class SimpleBinaryPerceptronTraining extends NetworkTraining
IPerceptronIterationEventBuffer $iterationEventBuffer, IPerceptronIterationEventBuffer $iterationEventBuffer,
string $sessionId, string $sessionId,
string $trainingId, string $trainingId,
?Closure $isCancelled = null,
) { ) {
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId); parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId, $isCancelled);
$this->perceptron = new SimpleBinaryPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize())); $this->perceptron = new SimpleBinaryPerceptron($synapticWeightsProvider->generate($datasetReader->getInputSize()));
} }
+3
View File
@@ -23,6 +23,9 @@ return Application::configure(basePath: dirname(__DIR__))
HandleInertiaRequests::class, HandleInertiaRequests::class,
AddLinkHeadersForPreloadedAssets::class, AddLinkHeadersForPreloadedAssets::class,
]); ]);
$middleware->trustProxies(at: [
'*'
]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// //
+4 -4
View File
@@ -36,10 +36,10 @@ return [
'secret' => env('REVERB_APP_SECRET'), 'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'), 'app_id' => env('REVERB_APP_ID'),
'options' => [ 'options' => [
'host' => env('REVERB_HOST'), 'host' => env('REVERB_BROADCAST_HOST', env('REVERB_HOST')),
'port' => env('REVERB_PORT', 443), 'port' => env('REVERB_BROADCAST_PORT', env('REVERB_PORT', 443)),
'scheme' => env('REVERB_SCHEME', 'https'), 'scheme' => env('REVERB_BROADCAST_SCHEME', env('REVERB_SCHEME', 'https')),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https', 'useTLS' => env('REVERB_BROADCAST_SCHEME', env('REVERB_SCHEME', 'https')) === 'https',
], ],
'client_options' => [ 'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
+1 -1
View File
@@ -22,7 +22,7 @@ return [
/** /**
* Minimum time between training progress broadcasts, in milliseconds. * Minimum time between training progress broadcasts, in milliseconds.
*/ */
'broadcast_minimum_interval_ms' => 0, 'broadcast_minimum_interval_ms' => 100,
/** /**
* Maximum number of weights for which all iteration weights are broadcast * Maximum number of weights for which all iteration weights are broadcast
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3
View File
@@ -1,5 +1,8 @@
#!/bin/sh #!/bin/sh
# Migrate the database
php ./artisan migrate --force || exit 1
# Cache # Cache
php ./artisan optimize:clear && php ./artisan optimize php ./artisan optimize:clear && php ./artisan optimize
+6
View File
@@ -6,6 +6,7 @@
"": { "": {
"dependencies": { "dependencies": {
"@inertiajs/vue3": "^2.3.7", "@inertiajs/vue3": "^2.3.7",
"@jaseeey/vue-umami-plugin": "^1.6.1",
"@lucide/vue": "^1.46.0", "@lucide/vue": "^1.46.0",
"@tailwindcss/typography": "^0.5.20", "@tailwindcss/typography": "^0.5.20",
"@vee-validate/zod": "^4.15.1", "@vee-validate/zod": "^4.15.1",
@@ -861,6 +862,11 @@
"@swc/helpers": "^0.5.0" "@swc/helpers": "^0.5.0"
} }
}, },
"node_modules/@jaseeey/vue-umami-plugin": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jaseeey/vue-umami-plugin/-/vue-umami-plugin-1.6.1.tgz",
"integrity": "sha512-a38bB7cYbwMP5HmlrT2Dk2RHdGFxVvLRci2GhY8cIwCpM419qXtvSM6SKmIJ3U3u7zYdcQLVAySOcXY150Z14Q=="
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+1
View File
@@ -37,6 +37,7 @@
}, },
"dependencies": { "dependencies": {
"@inertiajs/vue3": "^2.3.7", "@inertiajs/vue3": "^2.3.7",
"@jaseeey/vue-umami-plugin": "^1.6.1",
"@lucide/vue": "^1.46.0", "@lucide/vue": "^1.46.0",
"@tailwindcss/typography": "^0.5.20", "@tailwindcss/typography": "^0.5.20",
"@vee-validate/zod": "^4.15.1", "@vee-validate/zod": "^4.15.1",
+1 -1
View File
@@ -59,7 +59,7 @@ Reprenons le dataset oblique afin de pouvoir comparer les résultats, et diminuo
Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts. Puis cliquez sur "Lancer" tout en laissant les autres paramètres intacts.
On peut voir en dessous du tableau que l'entraînement s'est arrêté car le nombre maximal d'époques a été atteint. On pourrait l'augmenter (max 5000 pour mon petit serveur), mais ce serait du temps perdu. La solution du réseau est suffisante et il n'y aurait pas beaucoup de gain, même pour le triple d'itérations maximales en plus. Pour le prouver, cliquez sur le bouton `Afficher uniquement l'erreur quadratique moyenne`, la ligne que l'on peut voir ressemble fortement à la fonction logarithme $y = log(x^{-1})$. La progression de la descente du gradient est donc logarithmique ; les plus gros changements se font dans les premières époques. On peut voir en dessous du tableau que l'entraînement s'est arrêté car le nombre maximal d'époques a été atteint. On pourrait l'augmenter (max 5000 pour mon petit serveur), mais ce serait du temps perdu. La solution du réseau est suffisante et il n'y aurait pas beaucoup de gain, même pour le triple d'itérations maximales en plus. Pour le prouver, cliquez sur le bouton `Afficher uniquement l'erreur quadratique moyenne`, la ligne que l'on peut voir ressemble fortement à la fonction logarithme $y = log(x^{-1})$ (attention à l'échelle qui est elle-même logarithmique). La progression de la descente du gradient est donc logarithmique ; les plus gros changements se font dans les premières époques.
</div> </div>
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 205 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+15
View File
@@ -1,4 +1,5 @@
import { createInertiaApp } from '@inertiajs/vue3'; import { createInertiaApp } from '@inertiajs/vue3';
import { VueUmamiPlugin } from '@jaseeey/vue-umami-plugin';
import { configureEcho } from '@laravel/echo-vue'; import { configureEcho } from '@laravel/echo-vue';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import type { DefineComponent } from 'vue'; import type { DefineComponent } from 'vue';
@@ -22,6 +23,20 @@ createInertiaApp({
setup({ el, App, props, plugin }) { setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) }) createApp({ render: () => h(App, props) })
.use(plugin) .use(plugin)
.use(
VueUmamiPlugin({
websiteID: 'e616767f-e0d3-4ce9-b551-33c7c3806fc0',
scriptSrc: 'https://abcd.matthiasg.dev/script.js',
// autoTrack: true,
debug: import.meta.env.DEV,
extraDataAttributes: {
'data-auto-track': 'true',
'data-performance': 'true',
'data-domains': 'perceptron.matthiasg.dev,matthiasg.dev,www.matthiasg.dev',
'data-exclude-search': 'false',
}
}),
)
.mount(el); .mount(el);
}, },
progress: { progress: {
+2 -4
View File
@@ -1,13 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { Monitor, Moon, Sun } from 'lucide-vue-next'; import { Moon } from 'lucide-vue-next';
import { useAppearance } from '@/composables/useAppearance'; import { useAppearance } from '@/composables/useAppearance';
const { appearance, updateAppearance } = useAppearance(); const { appearance, updateAppearance } = useAppearance();
const tabs = [ const tabs = [
{ value: 'light', Icon: Sun, label: 'Light' },
{ value: 'dark', Icon: Moon, label: 'Dark' }, { value: 'dark', Icon: Moon, label: 'Dark' },
{ value: 'system', Icon: Monitor, label: 'System' },
] as const; ] as const;
</script> </script>
@@ -18,7 +16,7 @@ const tabs = [
<button <button
v-for="{ value, Icon, label } in tabs" v-for="{ value, Icon, label } in tabs"
:key="value" :key="value"
@click="updateAppearance(value)" @click="updateAppearance()"
:class="[ :class="[
'flex items-center rounded-md px-3.5 py-1.5 transition-colors', 'flex items-center rounded-md px-3.5 py-1.5 transition-colors',
appearance === value appearance === value
@@ -113,8 +113,8 @@ const datasets = computed<ErrorDataset[]>(() => {
min: 0, min: 0,
}, },
y: { y: {
type: !epochErrorOnly ? 'linear' : 'logarithmic',
stacked: true, stacked: true,
beginAtZero: !props.isRegression,
grid: { grid: {
color: function (context) { color: function (context) {
if (context.tick.value == 0) { if (context.tick.value == 0) {
+112 -27
View File
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useForm } from '@inertiajs/vue3'; import { useForm } from '@inertiajs/vue3';
import { trackUmamiEvent } from '@jaseeey/vue-umami-plugin';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { import {
Form, Form,
@@ -12,6 +13,7 @@ import {
NativeSelect, NativeSelect,
NativeSelectOption, NativeSelectOption,
} from '@/components/ui/native-select'; } from '@/components/ui/native-select';
import { cancel } from '@/routes/perceptron';
import type { import type {
Dataset, Dataset,
InitializationMethod, InitializationMethod,
@@ -23,8 +25,9 @@ import Card from './ui/card/Card.vue';
import CardContent from './ui/card/CardContent.vue'; import CardContent from './ui/card/CardContent.vue';
import CardHeader from './ui/card/CardHeader.vue'; import CardHeader from './ui/card/CardHeader.vue';
import CardTitle from './ui/card/CardTitle.vue'; import CardTitle from './ui/card/CardTitle.vue';
import Input from './ui/input/Input.vue';
import FormError from './ui/form/FormError.vue'; import FormError from './ui/form/FormError.vue';
import Input from './ui/input/Input.vue';
import Spinner from './ui/spinner/Spinner.vue';
const props = defineProps<{ const props = defineProps<{
type: PerceptronType; type: PerceptronType;
@@ -98,43 +101,69 @@ watch(maxIterations, (value) => {
watch(selectedDatasetCopy, (newvalue) => { watch(selectedDatasetCopy, (newvalue) => {
form.clearErrors('dataset'); form.clearErrors('dataset');
const selectedDatasetCopy = props.datasets.find( const selectedDatasetCopy =
(dataset) => dataset.label === newvalue props.datasets.find((dataset) => dataset.label === newvalue) || null;
) || null;
// LearningRate // LearningRate
learningRate.value = props.defaultLearningRate; learningRate.value = props.defaultLearningRate;
if (selectedDatasetCopy && selectedDatasetCopy.defaultLearningRate !== undefined) { if (
selectedDatasetCopy &&
selectedDatasetCopy.defaultLearningRate !== undefined
) {
learningRate.value = selectedDatasetCopy.defaultLearningRate; learningRate.value = selectedDatasetCopy.defaultLearningRate;
} }
// MinError // MinError
minError.value = props.minError; minError.value = props.minError;
if (selectedDatasetCopy && selectedDatasetCopy.defaultMinError !== undefined) { if (
selectedDatasetCopy &&
selectedDatasetCopy.defaultMinError !== undefined
) {
minError.value = selectedDatasetCopy.defaultMinError; minError.value = selectedDatasetCopy.defaultMinError;
} }
// MaxIterations // MaxIterations
maxIterations.value = props.defaultMaxIterations; maxIterations.value = props.defaultMaxIterations;
if (selectedDatasetCopy && selectedDatasetCopy.defaultMaxIterations !== undefined) { if (
selectedDatasetCopy &&
selectedDatasetCopy.defaultMaxIterations !== undefined
) {
maxIterations.value = selectedDatasetCopy.defaultMaxIterations; maxIterations.value = selectedDatasetCopy.defaultMaxIterations;
} }
// HiddenLayers // HiddenLayers
hiddenLayers.value = props.hiddenLayers; hiddenLayers.value = props.hiddenLayers;
if (selectedDatasetCopy && selectedDatasetCopy.defaultHiddenLayers !== undefined) { if (
selectedDatasetCopy &&
selectedDatasetCopy.defaultHiddenLayers !== undefined
) {
hiddenLayers.value = selectedDatasetCopy.defaultHiddenLayers; hiddenLayers.value = selectedDatasetCopy.defaultHiddenLayers;
} }
hiddenLayersNeurons.value = props.hiddenLayersNeurons; hiddenLayersNeurons.value = props.hiddenLayersNeurons;
if (selectedDatasetCopy && selectedDatasetCopy.defaultHiddenLayersNeurons !== undefined) { if (
hiddenLayersNeurons.value = selectedDatasetCopy.defaultHiddenLayersNeurons; selectedDatasetCopy &&
selectedDatasetCopy.defaultHiddenLayersNeurons !== undefined
) {
hiddenLayersNeurons.value =
selectedDatasetCopy.defaultHiddenLayersNeurons;
} }
}) });
const trainingId = ref<string>(''); const trainingId = ref<string>('');
function startTraining() { function startTraining() {
trackUmamiEvent('perceptron-training-start', {
type: props.type,
dataset: selectedDatasetCopy.value,
weight_init_method: selectedMethod.value,
hidden_layers: hiddenLayers.value,
hidden_layers_neurons: hiddenLayersNeurons.value,
min_error: minError.value,
learning_rate: learningRate.value,
max_iterations: maxIterations.value,
});
if (!selectedDatasetCopy.value) { if (!selectedDatasetCopy.value) {
form.setError( form.setError(
'dataset', 'dataset',
'Un dataset est nécessaire avant de lancer l\'entraînement.', "Un dataset est nécessaire avant de lancer l'entraînement.",
); );
console.log(form.errors); console.log(form.errors);
return; return;
@@ -156,19 +185,33 @@ function startTraining() {
max_iterations: maxIterations.value, max_iterations: maxIterations.value,
}); });
console.debug('[max_iterations] submitting', {
displayed: maxIterationsInput.value?.inputElement?.value,
refValue: maxIterations.value,
formValue: form.max_iterations,
limit: props.maxIterationsLimit,
});
form.post('/perceptron/run', { form.post('/perceptron/run', {
preserveScroll: true, preserveScroll: true,
}); });
} }
const emit = defineEmits(['update:selectedDataset', 'update:trainingId']); const emit = defineEmits(['update:selectedDataset', 'update:trainingId']);
async function cancelTraining() {
trackUmamiEvent('perceptron-training-cancel', {
training_id: trainingId.value,
});
form.cancel();
await fetch(cancel().url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify({
training_id: trainingId.value,
}),
});
}
watch(selectedDatasetCopy, (newValue) => { watch(selectedDatasetCopy, (newValue) => {
emit('update:selectedDataset', newValue); emit('update:selectedDataset', newValue);
}); });
@@ -182,6 +225,7 @@ watch(selectedDatasetCopy, (newValue) => {
<CardContent> <CardContent>
<Form <Form
class="grid auto-cols-max grid-flow-row grid-cols-1 gap-4 space-y-6 md:grid-cols-2" class="grid auto-cols-max grid-flow-row grid-cols-1 gap-4 space-y-6 md:grid-cols-2"
cancel-on-unmount
> >
<!-- DATASET --> <!-- DATASET -->
<FormField name="dataset"> <FormField name="dataset">
@@ -206,7 +250,12 @@ watch(selectedDatasetCopy, (newValue) => {
</NativeSelectOption> </NativeSelectOption>
</NativeSelect> </NativeSelect>
</FormControl> </FormControl>
<FormError :error="form.errors.dataset || props.errors?.selectedDatasetCopy" /> <FormError
:error="
form.errors.dataset ||
props.errors?.selectedDatasetCopy
"
/>
</FormItem> </FormItem>
</FormField> </FormField>
@@ -224,7 +273,9 @@ watch(selectedDatasetCopy, (newValue) => {
class="cursor-pointer" class="cursor-pointer"
> >
<NativeSelectOption <NativeSelectOption
v-for="method in (props.type == 'multilayer' ? ['random'] : ['zeros', 'random'])" v-for="method in props.type == 'multilayer'
? ['random']
: ['zeros', 'random']"
v-bind:key="method" v-bind:key="method"
:value="method" :value="method"
> >
@@ -236,7 +287,10 @@ watch(selectedDatasetCopy, (newValue) => {
</FormField> </FormField>
<!-- HIDDEN LAYERS --> <!-- HIDDEN LAYERS -->
<FormField name="hidden_layers" v-if="props.type === 'multilayer'"> <FormField
name="hidden_layers"
v-if="props.type === 'multilayer'"
>
<FormItem> <FormItem>
<FormLabel>Nombre de couches cachées</FormLabel> <FormLabel>Nombre de couches cachées</FormLabel>
<FormControl> <FormControl>
@@ -255,9 +309,14 @@ watch(selectedDatasetCopy, (newValue) => {
</FormField> </FormField>
<!-- HIDDEN LAYERS NEURONS --> <!-- HIDDEN LAYERS NEURONS -->
<FormField name="hidden_layers_neurons" v-if="props.type === 'multilayer'"> <FormField
name="hidden_layers_neurons"
v-if="props.type === 'multilayer'"
>
<FormItem> <FormItem>
<FormLabel>Nombre de neurones par couche cachée</FormLabel> <FormLabel
>Nombre de neurones par couche cachée</FormLabel
>
<FormControl> <FormControl>
<!-- TODO : MAX input --> <!-- TODO : MAX input -->
<Input <Input
@@ -319,13 +378,39 @@ watch(selectedDatasetCopy, (newValue) => {
@input="handleMaxIterationsInput" @input="handleMaxIterationsInput"
/> />
</FormControl> </FormControl>
<div v-if="form.errors.max_iterations || props.errors.max_iterations"> <div
{{ form.errors.max_iterations || props.errors.max_iterations }} v-if="
form.errors.max_iterations ||
props.errors.max_iterations
"
>
{{
form.errors.max_iterations ||
props.errors.max_iterations
}}
</div> </div>
</FormItem> </FormItem>
</FormField> </FormField>
</Form> </Form>
<Button variant="outline" class="cursor-pointer mt-6" @click="startTraining">Lancer</Button>
<Transition name="fade">
<Button
variant="outline"
class="mt-6 cursor-pointer"
:disabled="form.processing"
@click="startTraining"
>Lancer<Spinner v-if="form.processing" class="ml-1"
/></Button>
</Transition>
<Transition name="fade">
<Button
variant="outline"
class="mt-6 ml-4 cursor-pointer"
@click="cancelTraining"
v-if="form.processing"
>Annuler</Button
>
</Transition>
</CardContent> </CardContent>
</Card> </Card>
</template> </template>
+11 -73
View File
@@ -10,24 +10,12 @@ export type UseAppearanceReturn = {
updateAppearance: (value: Appearance) => void; updateAppearance: (value: Appearance) => void;
}; };
export function updateTheme(value: Appearance): void { export function updateTheme(): void {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return; return;
} }
if (value === 'system') { document.documentElement.classList.add('dark');
const mediaQueryList = window.matchMedia(
'(prefers-color-scheme: dark)',
);
const systemTheme = mediaQueryList.matches ? 'dark' : 'light';
document.documentElement.classList.toggle(
'dark',
systemTheme === 'dark',
);
} else {
document.documentElement.classList.toggle('dark', value === 'dark');
}
} }
const setCookie = (name: string, value: string, days = 365) => { const setCookie = (name: string, value: string, days = 365) => {
@@ -40,80 +28,30 @@ const setCookie = (name: string, value: string, days = 365) => {
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`; document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
}; };
const mediaQuery = () => {
if (typeof window === 'undefined') {
return null;
}
return window.matchMedia('(prefers-color-scheme: dark)');
};
const getStoredAppearance = () => {
if (typeof window === 'undefined') {
return null;
}
return localStorage.getItem('appearance') as Appearance | null;
};
const prefersDark = (): boolean => {
if (typeof window === 'undefined') {
return false;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
const handleSystemThemeChange = () => {
const currentAppearance = getStoredAppearance();
updateTheme(currentAppearance || 'system');
};
export function initializeTheme(): void { export function initializeTheme(): void {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return; return;
} }
// Initialize theme from saved preference or default to system... updateTheme();
const savedAppearance = getStoredAppearance();
updateTheme(savedAppearance || 'system');
// Set up system theme change listener...
mediaQuery()?.addEventListener('change', handleSystemThemeChange);
} }
const appearance = ref<Appearance>('system'); const appearance = ref<Appearance>('dark');
export function useAppearance(): UseAppearanceReturn { export function useAppearance(): UseAppearanceReturn {
onMounted(() => { onMounted(() => {
const savedAppearance = localStorage.getItem( appearance.value = 'dark';
'appearance',
) as Appearance | null;
if (savedAppearance) {
appearance.value = savedAppearance;
}
}); });
const resolvedAppearance = computed<ResolvedAppearance>(() => { const resolvedAppearance = computed<ResolvedAppearance>(() => {
if (appearance.value === 'system') { return 'dark';
return prefersDark() ? 'dark' : 'light';
}
return appearance.value;
}); });
function updateAppearance(value: Appearance) { function updateAppearance() {
appearance.value = value; appearance.value = 'dark';
localStorage.setItem('appearance', 'dark');
// Store in localStorage for client-side persistence... setCookie('appearance', 'dark');
localStorage.setItem('appearance', value); updateTheme();
// Store in cookie for SSR...
setCookie('appearance', value);
updateTheme(value);
} }
return { return {
+1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import AppLayout from '@/layouts/app/AppSidebarLayout.vue'; import AppLayout from '@/layouts/app/AppSidebarLayout.vue';
import type { BreadcrumbItem } from '@/types'; import type { BreadcrumbItem } from '@/types';
import { useColorMode, useDark } from '@vueuse/core';
type Props = { type Props = {
breadcrumbs?: BreadcrumbItem[]; breadcrumbs?: BreadcrumbItem[];
+19 -8
View File
@@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Head } from '@inertiajs/vue3'; import { Head } from '@inertiajs/vue3';
import { trackUmamiEvent } from '@jaseeey/vue-umami-plugin';
import { BookOpenText } from '@lucide/vue';
import { useEventListener } from '@vueuse/core'; import { useEventListener } from '@vueuse/core';
import { import {
Chart as ChartJS, Chart as ChartJS,
@@ -11,11 +13,23 @@ import {
LinearScale, LinearScale,
PointElement, PointElement,
LineElement, LineElement,
ScatterController,
LineController,
BarController,
LogarithmicScale,
} from 'chart.js'; } from 'chart.js';
import { ArrowDown, ArrowUp } from 'lucide-vue-next'; import { ArrowDown, ArrowUp } from 'lucide-vue-next';
import { computed, nextTick, ref, watch } from 'vue'; import { computed, nextTick, ref, watch } from 'vue';
import HelpText from '@/components/HelpText.vue';
import LinkHeader from '@/components/LinkHeader.vue'; import LinkHeader from '@/components/LinkHeader.vue';
import Button from '@/components/ui/button/Button.vue'; import Button from '@/components/ui/button/Button.vue';
import Drawer from '@/components/ui/drawer/Drawer.vue';
import DrawerContent from '@/components/ui/drawer/DrawerContent.vue';
import DrawerTitle from '@/components/ui/drawer/DrawerTitle.vue';
import DrawerTrigger from '@/components/ui/drawer/DrawerTrigger.vue';
import Kbd from '@/components/ui/kbd/Kbd.vue';
import KbdGroup from '@/components/ui/kbd/KbdGroup.vue';
import ScrollArea from '@/components/ui/scroll-area/ScrollArea.vue'; import ScrollArea from '@/components/ui/scroll-area/ScrollArea.vue';
import { import {
Tooltip as UiTooltip, Tooltip as UiTooltip,
@@ -29,14 +43,6 @@ import IterationTable from '../components/IterationTable.vue';
import PerceptronDecisionGraph from '../components/PerceptronDecisionGraph.vue'; import PerceptronDecisionGraph from '../components/PerceptronDecisionGraph.vue';
import PerceptronIterationsErrorsGraph from '../components/PerceptronIterationsErrorsGraph.vue'; import PerceptronIterationsErrorsGraph from '../components/PerceptronIterationsErrorsGraph.vue';
import PerceptronSetup from '../components/PerceptronSetup.vue'; import PerceptronSetup from '../components/PerceptronSetup.vue';
import HelpText from '@/components/HelpText.vue';
import { BookOpenText } from '@lucide/vue';
import Drawer from '@/components/ui/drawer/Drawer.vue';
import DrawerTrigger from '@/components/ui/drawer/DrawerTrigger.vue';
import DrawerContent from '@/components/ui/drawer/DrawerContent.vue';
import DrawerTitle from '@/components/ui/drawer/DrawerTitle.vue';
import KbdGroup from '@/components/ui/kbd/KbdGroup.vue';
import Kbd from '@/components/ui/kbd/Kbd.vue';
ChartJS.register( ChartJS.register(
Title, Title,
@@ -47,6 +53,10 @@ ChartJS.register(
LinearScale, LinearScale,
PointElement, PointElement,
LineElement, LineElement,
ScatterController,
LineController,
BarController,
LogarithmicScale
); );
ChartJS.defaults.font.size = 16; ChartJS.defaults.font.size = 16;
ChartJS.defaults.color = '#FFF'; ChartJS.defaults.color = '#FFF';
@@ -137,6 +147,7 @@ const handleDrawerOpenChange = async (open: boolean) => {
if (open) { if (open) {
await restoreHelpScroll(); await restoreHelpScroll();
trackUmamiEvent('wiki-opened', { perceptronType: props.type });
} }
}; };
+2 -2
View File
@@ -1,4 +1,4 @@
export type Appearance = 'light' | 'dark' | 'system'; export type Appearance = 'dark';
export type ResolvedAppearance = 'light' | 'dark'; export type ResolvedAppearance = 'dark';
export type AppShellVariant = 'header' | 'sidebar'; export type AppShellVariant = 'header' | 'sidebar';
+3 -19
View File
@@ -1,31 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @class(['dark' => ($appearance ?? 'system') == 'dark'])> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
{{-- Inline script to detect system dark mode preference and apply it immediately --}} {{-- Anonymous analytics --}}
<script> <script defer src="https://analytics.matthiasg.dev/recorder.js" data-website-id="e616767f-e0d3-4ce9-b551-33c7c3806fc0"></script>
(function() {
const appearance = '{{ $appearance ?? "system" }}';
if (appearance === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (prefersDark) {
document.documentElement.classList.add('dark');
}
}
})();
</script>
{{-- Inline style to set the HTML background color based on our theme in app.css --}} {{-- Inline style to set the HTML background color based on our theme in app.css --}}
<style> <style>
html { html {
background-color: oklch(1 0 0);
}
html.dark {
background-color: oklch(0.145 0 0); background-color: oklch(0.145 0 0);
} }
</style> </style>
+6
View File
@@ -1,2 +1,8 @@
<?php <?php
use App\Http\Controllers\PerceptronController;
use Illuminate\Support\Facades\Route;
Route::post('perceptron/cancel', [PerceptronController::class, 'cancel'])
->name('perceptron.cancel');