Wiki
linter / quality (push) Successful in 5m1s
tests / ci (8.4) (push) Successful in 5m8s
tests / ci (8.5) (push) Successful in 6m31s

This commit is contained in:
2026-09-15 19:44:58 +02:00
parent ca6c9fe38c
commit 6d4fdffee9
82 changed files with 29906 additions and 151 deletions
+1 -1
View File
@@ -1,7 +1,6 @@
<script setup lang="ts">
import { Form } from '@inertiajs/vue3';
import { useTemplateRef } from 'vue';
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
import Heading from '@/components/Heading.vue';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';
@@ -17,6 +16,7 @@ import {
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
const passwordInput = useTemplateRef('passwordInput');
</script>
+96
View File
@@ -0,0 +1,96 @@
<script lang="ts" setup>
import MarkdownIt from 'markdown-it';
import markdownItKatex from 'markdown-it-katex';
import { ref, onMounted } from 'vue';
import 'katex/dist/katex.min.css';
const props = defineProps<{
markdownFileName: string;
}>();
const emit = defineEmits<{
loaded: [];
}>();
const html = ref<string>('');
const title = ref<string>('');
onMounted(() => {
const fullUrl = `/helptext/${props.markdownFileName}.md`;
fetch(fullUrl)
.then((res) => {
if (!res.ok) throw new Error('Failed to load file');
return res.text();
})
.then((markdown) => {
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
});
md.use(markdownItKatex);
md.renderer.rules.link_open = (
tokens,
index,
options,
env,
self,
) => {
const token = tokens[index];
token.attrSet('target', '_blank');
token.attrSet('rel', 'noopener noreferrer');
return self.renderToken(tokens, index, options);
};
html.value = md.render(markdown);
const firstHeading = html.value.match(/<h[1-6]>(.*?)<\/h[1-6]>/i);
title.value = firstHeading?.[1].trim() || '';
html.value = firstHeading
? html.value.replace(firstHeading[0], '')
: html.value;
emit('loaded');
})
.catch((err) => {
console.error('Could not load help file:', err);
html.value = '';
title.value = '';
});
});
</script>
<template>
<div class="p-6 w-full">
<h1 class="b-4 mb-7 text-4xl font-semibold">{{ title }}</h1>
<div
v-html="html"
class="max-w-none block prose prose-lg text-justify prose-headings:text-left dark:prose-invert prose-code:before:hidden prose-code:after:hidden"
role="region"
aria-label="Help text"
></div>
</div>
</template>
<style scoped>
.prose :deep(.katex) {
white-space: nowrap;
}
.prose :deep(.katex .vlist) {
font-size: 0.7em;
line-height: 0em;
}
</style>
<style>
/* Custom styles for the help text */
.exercice {
padding: 0.1px 1rem;
background-color: var(--accent);
border-radius: 0.5rem;
}
.exercice ul li span {
font-style: oblique;
}
</style>
+2 -2
View File
@@ -40,14 +40,14 @@ const rowBgDark = computed(() => {
<template>
<table class="table w-full border-collapse border border-gray-300">
<tr class="text-left" v-if="props.iterations.length > 0">
<th class="sticky top-0 z-10 bg-background">Époch</th>
<th class="sticky top-0 z-10 bg-background">Époque</th>
<th class="sticky top-0 z-10 bg-background">Exemple</th>
<th
v-for="(weight, index) in displayedWeights"
v-bind:key="index"
class="sticky top-0 z-10 bg-background"
>
X<sub>{{ index }}</sub>
w<sub>{{ index }}</sub>
</th>
<th class="sticky top-0 z-10 bg-background">Erreur</th>
</tr>
+13 -8
View File
@@ -6,42 +6,47 @@ const page = usePage();
const links = [
{
name: 'Perceptron Simple',
name: 'Introduction',
href: '/',
data: {},
},
{
name: '1. Perceptron Simple',
href: '/perceptron',
data: { type: 'simple' },
},
{
name: 'Descente du gradient',
name: '2. Descente du gradient',
href: '/perceptron',
data: { type: 'gradientdescent' },
},
{
name: 'ADALINE',
name: '3. ADALINE',
href: '/perceptron',
data: { type: 'adaline' },
},
{
name: 'Mono-couche',
name: '4. Monocouche',
href: '/perceptron',
data: { type: 'monolayer' },
},
{
name: 'Multi-couche',
name: '5. Multicouches',
href: '/perceptron',
data: { type: 'multilayer' },
},
];
const isActiveLink = (link: any) => {
return page.component === 'PerceptronViewer' && page.props.type === link.data.type;
return (page.component === 'Home' && page.props.type === link.data?.type) || (page.component === 'PerceptronViewer' && page.props.type === link.data?.type);
};
</script>
<template>
<header
class="my-6 w-full text-sm not-has-[nav]:hidden"
class="my-6 w-full overflow-x-auto text-sm not-has-[nav]:hidden"
>
<nav class="flex items-center justify-center gap-4">
<nav class="min-w-max flex items-center justify-center gap-4">
<Link
v-for="link in links"
:key="link.name"
@@ -129,8 +129,8 @@ const datasets = computed<ErrorDataset[]>(() => {
}"
:data="{
labels: props.iterations.reduce((labels, iteration) => {
if (!labels.includes(`Époch ${iteration.epoch}`)) {
labels.push(`Époch ${iteration.epoch}`);
if (!labels.includes(`Époque ${iteration.epoch}`)) {
labels.push(`Époque ${iteration.epoch}`);
}
return labels;
}, [] as string[]),
+85 -45
View File
@@ -1,6 +1,5 @@
<script setup lang="ts">
// import { Form } from '@inertiajs/vue3';
import { useForm } from '@inertiajs/vue3';
import { ref, watch } from 'vue';
import {
Form,
@@ -17,6 +16,7 @@ import type {
Dataset,
InitializationMethod,
PerceptronType,
ValidationErrors,
} from '@/types/perceptron';
import Button from './ui/button/Button.vue';
import Card from './ui/card/Card.vue';
@@ -36,6 +36,8 @@ const props = defineProps<{
defaultLearningRate: number;
sessionId: string;
defaultMaxIterations: number;
maxIterationsLimit: number;
errors: ValidationErrors;
}>();
const selectedDatasetCopy = ref(props.selectedDataset);
@@ -45,6 +47,52 @@ const hiddenLayersNeurons = ref(props.hiddenLayersNeurons);
const minError = ref(props.minError);
const learningRate = ref(props.defaultLearningRate);
const maxIterations = ref(props.defaultMaxIterations);
const maxIterationsInput = ref<{
inputElement: HTMLInputElement | null;
} | null>(null);
const form = useForm({
type: props.type,
dataset: '',
weight_init_method: selectedMethod.value,
hidden_layers: hiddenLayers.value,
hidden_layers_neurons: hiddenLayersNeurons.value,
min_error: minError.value,
learning_rate: learningRate.value,
session_id: props.sessionId,
training_id: '',
max_iterations: maxIterations.value,
});
function handleMaxIterationsInput(event: Event) {
const input = event.target as HTMLInputElement;
const numericValue = Number(input.value);
const normalizedValue = Number.isFinite(numericValue)
? Math.min(Math.max(numericValue, 1), props.maxIterationsLimit)
: 1;
console.debug('[max_iterations] native input', {
raw: input.value,
numericValue,
normalizedValue,
limit: props.maxIterationsLimit,
});
input.value = String(normalizedValue);
maxIterations.value = normalizedValue;
}
watch(maxIterations, (value) => {
const input = maxIterationsInput.value?.inputElement;
console.debug('[max_iterations] ref changed', {
value,
displayed: input?.value,
});
if (input && input.value !== String(value)) {
input.value = String(value);
}
});
watch(selectedDatasetCopy, (newvalue) => {
const selectedDatasetCopy = props.datasets.find(
@@ -85,46 +133,32 @@ function startTraining() {
return;
}
trainingId.value = `${props.sessionId}-${Date.now()}`; // Unique training ID based on session and timestamp
emit('update:trainingId', trainingId.value); // Emit the training ID to the parent component
trainingId.value = `${props.sessionId}-${Date.now()}`;
emit('update:trainingId', trainingId.value);
fetch('/api/perceptron/run', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify({
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,
session_id: props.sessionId,
training_id: trainingId.value,
max_iterations: maxIterations.value,
}),
})
.then((response) => {
if (!response.ok) {
if (response.status === 429) {
alert('Trop de requêtes pour mon petit serveur. Veuillez réessayer dans une minute.');
} else {
alert('Erreur lors du lancement de l\'entraînement. Veuillez réessayer.');
}
Object.assign(form, {
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,
session_id: props.sessionId,
training_id: trainingId.value,
max_iterations: maxIterations.value,
});
throw new Error('Network response was not ok');
}
return response.json();
})
.then((data) => {
console.log('Perceptron training started:', data);
})
.catch((error) => {
console.error('Error starting perceptron training:', error);
});
console.debug('[max_iterations] submitting', {
displayed: maxIterationsInput.value?.inputElement?.value,
refValue: maxIterations.value,
formValue: form.max_iterations,
limit: props.maxIterationsLimit,
});
form.post('/perceptron/run', {
preserveScroll: true,
});
}
const emit = defineEmits(['update:selectedDataset', 'update:trainingId']);
@@ -161,10 +195,11 @@ watch(selectedDatasetCopy, (newValue) => {
v-bind:key="dataset.label"
:value="dataset.label"
>
{{ dataset.label }}
{{ dataset.label.replace(/_/g, ' ') }}
</NativeSelectOption>
</NativeSelect>
</FormControl>
<div v-if="props.errors?.selectedDatasetCopy">{{ props.errors?.selectedDatasetCopy }}</div>
</FormItem>
</FormField>
@@ -200,6 +235,7 @@ watch(selectedDatasetCopy, (newValue) => {
<FormControl>
<!-- TODO : MAX input -->
<Input
ref="maxIterationsInput"
type="number"
v-model="hiddenLayers"
min="1"
@@ -264,17 +300,21 @@ watch(selectedDatasetCopy, (newValue) => {
<!-- MAX ITERATIONS -->
<FormField name="max_iterations">
<FormItem>
<FormLabel>Nombre maximum d'itérations</FormLabel>
<FormLabel>Nombre maximum d'époques</FormLabel>
<FormControl>
<Input
type="number"
v-model="maxIterations"
min="0"
max="5000"
:model-value="maxIterations"
min="1"
:max="props.maxIterationsLimit"
step="1"
class="w-min"
@input="handleMaxIterationsInput"
/>
</FormControl>
<div v-if="form.errors.max_iterations || props.errors.max_iterations">
{{ form.errors.max_iterations || props.errors.max_iterations }}
</div>
</FormItem>
</FormField>
</Form>
+1 -1
View File
@@ -9,8 +9,8 @@ import {
} from '@/components/ui/dropdown-menu';
import UserInfo from '@/components/UserInfo.vue';
import { logout } from '@/routes';
import { edit } from '@/routes/profile';
import type { User } from '@/types';
import { edit } from '@/routes/profile';
type Props = {
user: User;
@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { AccordionRootEmits, AccordionRootProps } from "reka-ui"
import {
AccordionRoot,
useForwardPropsEmits,
} from "reka-ui"
const props = defineProps<AccordionRootProps>()
const emits = defineEmits<AccordionRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<AccordionRoot v-slot="slotProps" data-slot="accordion" v-bind="forwarded">
<slot v-bind="slotProps" />
</AccordionRoot>
</template>
@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { AccordionContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { AccordionContent } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<AccordionContent
data-slot="accordion-content"
v-bind="delegatedProps"
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
>
<div :class="cn('pt-0 pb-4', props.class)">
<slot />
</div>
</AccordionContent>
</template>
@@ -0,0 +1,24 @@
<script setup lang="ts">
import type { AccordionItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { AccordionItem, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<AccordionItem
v-slot="slotProps"
data-slot="accordion-item"
v-bind="forwardedProps"
:class="cn('border-b last:border-b-0', props.class)"
>
<slot v-bind="slotProps" />
</AccordionItem>
</template>
@@ -0,0 +1,37 @@
<script setup lang="ts">
import type { AccordionTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { ChevronDown } from "@lucide/vue"
import { reactiveOmit } from "@vueuse/core"
import {
AccordionHeader,
AccordionTrigger,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<AccordionHeader class="flex">
<AccordionTrigger
data-slot="accordion-trigger"
v-bind="delegatedProps"
:class="
cn(
'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
props.class,
)
"
>
<slot />
<slot name="icon">
<ChevronDown
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
/>
</slot>
</AccordionTrigger>
</AccordionHeader>
</template>
@@ -0,0 +1,4 @@
export { default as Accordion } from "./Accordion.vue"
export { default as AccordionContent } from "./AccordionContent.vue"
export { default as AccordionItem } from "./AccordionItem.vue"
export { default as AccordionTrigger } from "./AccordionTrigger.vue"
@@ -27,6 +27,7 @@ export const buttonVariants = cva(
"icon": "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
"icon-xl": "size-13",
},
},
defaultVariants: {
@@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { DrawerRootEmits, DrawerRootProps } from "reka-ui"
import { DrawerRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DrawerRootProps>()
const emits = defineEmits<DrawerRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DrawerRoot
v-slot="slotProps"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</DrawerRoot>
</template>
@@ -0,0 +1,15 @@
<script lang="ts" setup>
import type { DrawerCloseProps } from "reka-ui"
import { DrawerClose } from "reka-ui"
const props = defineProps<DrawerCloseProps>()
</script>
<template>
<DrawerClose
data-slot="drawer-close"
v-bind="props"
>
<slot />
</DrawerClose>
</template>
@@ -0,0 +1,48 @@
<script lang="ts" setup>
import type { DrawerContentEmits, DrawerContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DrawerContent,
DrawerHandle,
DrawerPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
import DrawerOverlay from "./DrawerOverlay.vue"
defineOptions({
inheritAttrs: false,
})
const props = defineProps<DrawerContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DrawerContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DrawerPortal>
<DrawerOverlay />
<DrawerContent
data-slot="drawer-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="cn(
'group/drawer-content bg-background fixed z-50 flex h-auto flex-col',
'will-change-transform transform-[translate3d(var(--drawer-swipe-movement-x,0px),var(--drawer-swipe-movement-y,0px),0)]',
'transition-transform duration-500 ease-[cubic-bezier(0.32,0.72,0,1)] data-swiping:duration-0 data-swiping:select-none',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[swipe-direction=up]:inset-x-0 data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:mb-24 data-[swipe-direction=up]:max-h-[80vh] data-[swipe-direction=up]:rounded-b-lg data-[swipe-direction=up]:data-[state=open]:slide-in-from-top data-[swipe-direction=up]:data-[state=closed]:slide-out-to-top',
'data-[swipe-direction=down]:inset-x-0 data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:mt-24 data-[swipe-direction=down]:max-h-[80vh] data-[swipe-direction=down]:rounded-t-lg data-[swipe-direction=down]:data-[state=open]:slide-in-from-bottom data-[swipe-direction=down]:data-[state=closed]:slide-out-to-bottom',
'data-[swipe-direction=right]:inset-y-0 data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:w-3/4 data-[swipe-direction=right]:sm:max-w-sm data-[swipe-direction=right]:data-[state=open]:slide-in-from-right data-[swipe-direction=right]:data-[state=closed]:slide-out-to-right',
'data-[swipe-direction=left]:inset-y-0 data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:w-3/4 data-[swipe-direction=left]:sm:max-w-sm data-[swipe-direction=left]:data-[state=open]:slide-in-from-left data-[swipe-direction=left]:data-[state=closed]:slide-out-to-left',
props.class,
)"
>
<DrawerHandle class="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[swipe-direction=down]/drawer-content:block" />
<slot />
</DrawerContent>
</DrawerPortal>
</template>
@@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { DrawerDescriptionProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DrawerDescription } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DrawerDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DrawerDescription
data-slot="drawer-description"
v-bind="delegatedProps"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</DrawerDescription>
</template>
@@ -0,0 +1,17 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="drawer-footer"
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,17 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="drawer-header"
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,19 @@
<script lang="ts" setup>
import type { DrawerOverlayProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DrawerOverlay } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DrawerOverlayProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DrawerOverlay
data-slot="drawer-overlay"
v-bind="delegatedProps"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 duration-500', props.class)"
/>
</template>
@@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { DrawerTitleProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DrawerTitle } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DrawerTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DrawerTitle
data-slot="drawer-title"
v-bind="delegatedProps"
:class="cn('text-foreground font-semibold', props.class)"
>
<slot />
</DrawerTitle>
</template>
@@ -0,0 +1,15 @@
<script lang="ts" setup>
import type { DrawerTriggerProps } from "reka-ui"
import { DrawerTrigger } from "reka-ui"
const props = defineProps<DrawerTriggerProps>()
</script>
<template>
<DrawerTrigger
data-slot="drawer-trigger"
v-bind="props"
>
<slot />
</DrawerTrigger>
</template>
@@ -0,0 +1,9 @@
export { default as Drawer } from "./Drawer.vue"
export { default as DrawerClose } from "./DrawerClose.vue"
export { default as DrawerContent } from "./DrawerContent.vue"
export { default as DrawerDescription } from "./DrawerDescription.vue"
export { default as DrawerFooter } from "./DrawerFooter.vue"
export { default as DrawerHeader } from "./DrawerHeader.vue"
export { default as DrawerOverlay } from "./DrawerOverlay.vue"
export { default as DrawerTitle } from "./DrawerTitle.vue"
export { default as DrawerTrigger } from "./DrawerTrigger.vue"
+13 -6
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { useVModel } from "@vueuse/core"
import { ref } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
@@ -13,15 +13,22 @@ const emits = defineEmits<{
(e: "update:modelValue", payload: string | number): void
}>()
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
})
const inputElement = ref<HTMLInputElement | null>(null)
defineExpose({ inputElement })
function handleInput(event: Event) {
const input = event.target as HTMLInputElement
emits("update:modelValue", input.value)
}
</script>
<template>
<input
v-model="modelValue"
ref="inputElement"
:value="modelValue ?? defaultValue ?? ''"
v-bind="$attrs"
@input="handleInput"
data-slot="input"
:class="cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
+21
View File
@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<kbd
:class="cn(
'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none',
`[&_svg:not([class*='size-'])]:size-3`,
'[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10',
props.class,
)"
>
<slot />
</kbd>
</template>
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<kbd
data-slot="kbd-group"
:class="cn('inline-flex items-center gap-1', props.class)"
>
<slot />
</kbd>
</template>
+2
View File
@@ -0,0 +1,2 @@
export { default as Kbd } from "./Kbd.vue"
export { default as KbdGroup } from "./KbdGroup.vue"
+2 -2
View File
@@ -5,11 +5,11 @@ import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { useCurrentUrl } from '@/composables/useCurrentUrl';
import { toUrl } from '@/lib/utils';
import { show } from '@/routes/two-factor';
import type { NavItem } from '@/types';
import { edit as editAppearance } from '@/routes/appearance';
import { edit as editProfile } from '@/routes/profile';
import { show } from '@/routes/two-factor';
import { edit as editPassword } from '@/routes/user-password';
import type { NavItem } from '@/types';
const sidebarNavItems: NavItem[] = [
{
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts" setup>
import { Head } from '@inertiajs/vue3';
import HelpText from '@/components/HelpText.vue';
import LinkHeader from '@/components/LinkHeader.vue';
</script>
<template>
<Head title="Perceptron Viewer"></Head>
<main class="flex min-h-dvh flex-col gap-6 pb-6">
<LinkHeader class="w-full my-0! py-6 sticky top-0 z-10 bg-background" />
<div
class="flex flex-col items-center gap-6 rounded-lg p-6 shadow sm:p-8"
>
<HelpText markdownFileName="intro" class="max-w-[90ch]" />
</div>
</main>
</template>
+123 -22
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { useEventListener } from '@vueuse/core';
import {
Chart as ChartJS,
Title,
@@ -16,12 +17,26 @@ 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 } from '@/types/perceptron';
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,
@@ -44,10 +59,15 @@ const props = defineProps<{
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,
@@ -91,8 +111,42 @@ const {
trainingEndReason,
} = usePerceptronTraining(props.sessionId);
const isRegression = computed(
() => (props.type === 'multilayer' || props.type === 'monolayer') && activationFunction.value === 'linear',
() => 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':
@@ -118,31 +172,78 @@ watch([iterations, trainingEnded], async () => {
await nextTick();
iterationScrollArea.value?.followBottom();
});
</script>
<template>
<Head title="Perceptron Viewer"></Head>
<main class="flex min-h-dvh flex-col gap-6 pb-6 space-y-6">
<main class="flex min-h-dvh flex-col pb-6 space-y-6">
<LinkHeader class="w-full" />
<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"
@update:selected-dataset="
(newValue) => {
selectedDatasetName = newValue;
}
"
@update:training-id="setTrainingId"
/>
<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"
+3
View File
@@ -7,6 +7,7 @@ export type Iteration = {
export type Dataset = {
label: string;
inputCount: number;
data: DatasetPoint[];
defaultLearningRate?: number;
defaultMinError?: number;
@@ -24,3 +25,5 @@ export type DatasetPoint = {
export type InitializationMethod = 'zeros' | 'random';
export type PerceptronType = 'simple' | 'gradientdescent' | 'adaline' | 'monolayer' | 'multilayer';
export type ValidationErrors = Record<string, string>;