Wiki
This commit is contained in:
+15
-1
@@ -1,10 +1,10 @@
|
||||
@import 'tailwindcss';
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
@@ -170,3 +170,17 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.quill-content .ql-align-center { text-align: center; }
|
||||
.quill-content .ql-align-right { text-align: right; }
|
||||
.quill-content .ql-align-justify { text-align: justify; }
|
||||
|
||||
.quill-content .ql-size-small { font-size: 0.875rem; }
|
||||
.quill-content .ql-size-large { font-size: 1.25rem; }
|
||||
.quill-content .ql-size-huge { font-size: 1.5rem; }
|
||||
|
||||
.quill-content .ql-indent-1 { padding-left: 1.5rem; }
|
||||
.quill-content .ql-indent-2 { padding-left: 3rem; }
|
||||
.quill-content .ql-indent-3 { padding-left: 4.5rem; }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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[]),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as Kbd } from "./Kbd.vue"
|
||||
export { default as KbdGroup } from "./KbdGroup.vue"
|
||||
@@ -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[] = [
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user