Files
perceptron-viewer/resources/js/pages/PerceptronViewer.vue
T
Ninluc 25b03fc39a
linter / quality (push) Failing after 1m11s
tests / ci (8.3) (push) Successful in 3m55s
Cancel Training
2026-09-18 23:08:19 +02:00

341 lines
12 KiB
Vue

<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { useEventListener } from '@vueuse/core';
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
BarElement,
CategoryScale,
LinearScale,
PointElement,
LineElement,
ScatterController,
LineController,
BarController,
LogarithmicScale,
} from 'chart.js';
import { ArrowDown, ArrowUp } from 'lucide-vue-next';
import { computed, nextTick, ref, watch } from 'vue';
import LinkHeader from '@/components/LinkHeader.vue';
import Button from '@/components/ui/button/Button.vue';
import ScrollArea from '@/components/ui/scroll-area/ScrollArea.vue';
import {
Tooltip as UiTooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { usePerceptronTraining } from '@/composables/usePerceptronTraining';
import type { Dataset, DatasetPoint, InitializationMethod, PerceptronType, ValidationErrors } from '@/types/perceptron';
import IterationTable from '../components/IterationTable.vue';
import PerceptronDecisionGraph from '../components/PerceptronDecisionGraph.vue';
import PerceptronIterationsErrorsGraph from '../components/PerceptronIterationsErrorsGraph.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(
Title,
Tooltip,
Legend,
BarElement,
CategoryScale,
LinearScale,
PointElement,
LineElement,
ScatterController,
LineController,
BarController,
LogarithmicScale
);
ChartJS.defaults.font.size = 16;
ChartJS.defaults.color = '#FFF';
ChartJS.defaults.backgroundColor = '#AAA';
const props = defineProps<{
type: PerceptronType;
sessionId: string;
datasets: Dataset[];
minError: number;
learningRate: number;
maxIterations: number;
maxIterationsLimit: number;
maxDisplayedWeights: number;
errors: ValidationErrors;
}>();
const selectedDatasetName = ref<string>('');
const selectedDataset = computed(() =>
props.datasets.find((item) => item.label === selectedDatasetName.value),
);
const dataset = computed<DatasetPoint[]>(() => {
const selected = props.datasets.find(
(d) => d.label === selectedDatasetName.value,
);
return selected ? selected.data : [];
});
const cleanedDataset = computed<
{
label: number;
data: { x: number; y: number }[];
}[]
>(() => {
if (!dataset.value) {
return [];
}
const cleanedDataset: {
label: number;
data: { x: number; y: number }[];
}[] = [];
// Separate data into each dataset based on value of the last column (label)
dataset.value.forEach((row) => {
let dataset = cleanedDataset.find((d) => d.label === row.label);
if (!dataset) {
dataset = { label: row.label, data: [] };
cleanedDataset.push(dataset);
}
dataset.data.push({ x: row.x, y: row.y });
});
return cleanedDataset;
});
const hiddenLayers = ref(3);
const hiddenLayersNeurons = ref(3);
const initializationMethod = ref<InitializationMethod>(props.type === 'multilayer' ? 'random' : 'zeros');
const {
activationFunction,
iterations,
setTrainingId,
trainingEnded,
trainingEndReason,
} = usePerceptronTraining(props.sessionId);
const isRegression = computed(
() => selectedDataset.value?.inputCount === 1 && activationFunction.value === 'linear',
);
const drawerOpen = ref(false);
const helpScrollContainer = ref<HTMLElement | null>(null);
const helpScrollTop = ref(0);
const handleHelpScroll = (event: Event) => {
helpScrollTop.value = (event.currentTarget as HTMLElement).scrollTop;
};
const restoreHelpScroll = async () => {
await nextTick();
if (helpScrollContainer.value) {
helpScrollContainer.value.scrollTop = helpScrollTop.value;
}
};
const handleDrawerOpenChange = async (open: boolean) => {
if (!open && helpScrollContainer.value) {
helpScrollTop.value = helpScrollContainer.value.scrollTop;
}
drawerOpen.value = open;
if (open) {
await restoreHelpScroll();
}
};
useEventListener('keydown', (event: KeyboardEvent) => {
if (event.key.toLowerCase() === 'w' /* && (event.ctrlKey || event.metaKey)*/) {
event.preventDefault();
void handleDrawerOpenChange(!drawerOpen.value);
}
});
function getActivationFunction(type: string): (x: number) => number {
switch (type) {
case 'step':
return (x) => (x >= 0 ? 1 : 0);
case 'sigmoid':
return (x) => 1 / (1 + Math.exp(-x));
case 'tanh':
return (x) => Math.tanh(x);
case 'linear':
return (x) => x;
default:
return (x) => x; // Identity function as fallback
}
}
const iterationScrollArea = ref<{
followBottom: (force?: boolean) => void;
isFollowingBottom: boolean;
scrollToTop: (behavior?: ScrollBehavior) => void;
} | null>(null);
watch([iterations, trainingEnded], async () => {
await nextTick();
iterationScrollArea.value?.followBottom();
});
</script>
<template>
<Head title="Perceptron Viewer"></Head>
<main class="flex min-h-dvh flex-col pb-6 space-y-6">
<LinkHeader class="w-full" />
<Drawer
:open="drawerOpen"
swipe-direction="left"
@update:open="handleDrawerOpenChange"
>
<TooltipProvider :delay-duration="300">
<UiTooltip>
<TooltipTrigger as-child>
<DrawerTrigger as-child>
<Button
variant="default"
size="icon-xl"
class="sticky top-3 z-20 ml-3 cursor-pointer rounded-full"
title="Wiki"
aria-label="Wiki"
>
<BookOpenText class="size-7" />
</Button>
</DrawerTrigger>
</TooltipTrigger>
<TooltipContent side="right">
<span>Wiki </span>
<KbdGroup>
<Kbd>W</Kbd>
</KbdGroup>
</TooltipContent>
</UiTooltip>
</TooltipProvider>
<DrawerContent class="w-[100ch]! max-w-[85vw]!">
<DrawerTitle class="sr-only">Wiki</DrawerTitle>
<div
ref="helpScrollContainer"
class="no-scrollbar overflow-y-auto px-4"
@scroll="handleHelpScroll"
>
<HelpText
:markdownFileName="props.type"
@loaded="restoreHelpScroll"
/>
</div>
</DrawerContent>
</Drawer>
<PerceptronSetup
:type="props.type"
:datasets="props.datasets"
:selectedDataset="selectedDatasetName"
:initializationMethod="initializationMethod"
:hidden-layers="hiddenLayers"
:hidden-layers-neurons="hiddenLayersNeurons"
:minError="props.minError"
:sessionId="props.sessionId"
:defaultLearningRate="props.learningRate"
:defaultMaxIterations="props.maxIterations"
:maxIterationsLimit="props.maxIterationsLimit"
:errors="props.errors"
@update:selected-dataset="
(newValue) => {
selectedDatasetName = newValue;
}
"
@update:training-id="setTrainingId"
/>
<div
class="align-items-start justify-content-center grid max-w-dvw grid-cols-2"
v-if="selectedDatasetName || iterations.length > 0"
>
<div class="pt-3 h-dvh max-h-dvh min-h-0 w-full min-w-0">
<ScrollArea ref="iterationScrollArea" :keep-bottom="true" type="scroll" class="relative h-full max-h-full min-h-0 w-full">
<IterationTable
:iterations="iterations"
:trainingEnded="trainingEnded"
:trainingEndReason="trainingEndReason"
:maxDisplayedWeights="props.maxDisplayedWeights"
/>
<template #overlay>
<div class="absolute right-4 bottom-4 z-10 flex flex-col items-end gap-2">
<Button
type="button"
variant="secondary"
size="icon"
class="size-9 rounded-full shadow-md"
aria-label="Revenir en haut du tableau"
title="Revenir en haut"
@click="iterationScrollArea?.scrollToTop()"
>
<ArrowUp class="size-4" />
</Button>
<Transition
enter-active-class="transition-opacity duration-300 ease-in-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition-opacity duration-300 ease-in-out"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<Button
v-if="iterationScrollArea && !iterationScrollArea.isFollowingBottom"
type="button"
variant="secondary"
size="icon"
class="size-9 rounded-full shadow-md"
aria-label="Reprendre le défilement automatique"
title="Reprendre le défilement automatique"
@click="iterationScrollArea?.followBottom(true)"
>
<ArrowDown class="size-4" />
</Button>
</Transition>
</div>
</template>
</ScrollArea>
</div>
<div class="h-full w-full min-w-0 space-y-6">
<div>
<PerceptronDecisionGraph
:cleanedDataset="cleanedDataset"
:iterations="iterations"
:is-regression="isRegression"
:activation-function="
getActivationFunction(activationFunction)
"
/>
</div>
<div>
<PerceptronIterationsErrorsGraph
:iterations="iterations"
:is-regression="isRegression"
v-if="iterations.length > 0"
/>
</div>
</div>
</div>
</main>
</template>
<style lang="css" scoped>
/* we will explain what these classes do next! */
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>