341 lines
12 KiB
Vue
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>
|