Reworked the table scroll
This commit is contained in:
Generated
+21
-8
@@ -6,6 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inertiajs/vue3": "^2.3.7",
|
"@inertiajs/vue3": "^2.3.7",
|
||||||
|
"@lucide/vue": "^1.43.0",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"lucide-vue-next": "^0.468.0",
|
"lucide-vue-next": "^0.468.0",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"reka-ui": "^2.9.2",
|
"reka-ui": "^2.10.4",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^4.1.1",
|
"tailwindcss": "^4.1.1",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
@@ -925,6 +926,15 @@
|
|||||||
"integrity": "sha512-yZYIr1iwuCQ7LFI+GsJk9vacw1HWMp3ZlDlW0pdfz3zXyKeu4US7oH79KmQQ031L0cYaSyaUMo/Ha1D4BosKqw==",
|
"integrity": "sha512-yZYIr1iwuCQ7LFI+GsJk9vacw1HWMp3ZlDlW0pdfz3zXyKeu4US7oH79KmQQ031L0cYaSyaUMo/Ha1D4BosKqw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@lucide/vue": {
|
||||||
|
"version": "1.43.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.43.0.tgz",
|
||||||
|
"integrity": "sha512-Asq2HozXp4s7JxQVTVVeMo6WINVsJr477uNOafM3Qft1K1aDbbgkVuCP3X37UZXcVB4rEGXaIM//04Ndi2GJfQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": ">=3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "0.2.12",
|
"version": "0.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||||
@@ -3830,6 +3840,7 @@
|
|||||||
"version": "12.8.2",
|
"version": "12.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz",
|
||||||
"integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==",
|
"integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/web-bluetooth": "^0.0.21",
|
"@types/web-bluetooth": "^0.0.21",
|
||||||
"@vueuse/metadata": "12.8.2",
|
"@vueuse/metadata": "12.8.2",
|
||||||
@@ -4490,9 +4501,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/defu": {
|
"node_modules/defu": {
|
||||||
"version": "6.1.4",
|
"version": "6.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
||||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
|
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -7386,9 +7398,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/reka-ui": {
|
"node_modules/reka-ui": {
|
||||||
"version": "2.9.2",
|
"version": "2.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.10.4.tgz",
|
||||||
"integrity": "sha512-/t4e6y1hcG+uDuRfpg6tbMz3uUEvRzNco6NeYTufoJeUghy5Iosxos5YL/p+ieAsid84sdMX9OrgDqpEuCJhBw==",
|
"integrity": "sha512-kbS5GAbkHkYj0EVKAg5ZPEPndhBjeUFSa1Aq5m5ftQwyHnB1v5tw8X6fcwfKH/ry3StPXblMWM1DW82MVS71cw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/dom": "^1.6.13",
|
"@floating-ui/dom": "^1.6.13",
|
||||||
"@floating-ui/vue": "^1.1.6",
|
"@floating-ui/vue": "^1.1.6",
|
||||||
@@ -7398,7 +7411,7 @@
|
|||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"@vueuse/shared": "^14.1.0",
|
"@vueuse/shared": "^14.1.0",
|
||||||
"aria-hidden": "^1.2.4",
|
"aria-hidden": "^1.2.4",
|
||||||
"defu": "^6.1.4",
|
"defu": "^6.1.5",
|
||||||
"ohash": "^2.0.11"
|
"ohash": "^2.0.11"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
|
|||||||
+2
-1
@@ -37,6 +37,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inertiajs/vue3": "^2.3.7",
|
"@inertiajs/vue3": "^2.3.7",
|
||||||
|
"@lucide/vue": "^1.43.0",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
@@ -45,7 +46,7 @@
|
|||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"lucide-vue-next": "^0.468.0",
|
"lucide-vue-next": "^0.468.0",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"reka-ui": "^2.9.2",
|
"reka-ui": "^2.10.4",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^4.1.1",
|
"tailwindcss": "^4.1.1",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
|
|||||||
@@ -40,15 +40,16 @@ const rowBgDark = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<table class="table w-full border-collapse border border-gray-300">
|
<table class="table w-full border-collapse border border-gray-300">
|
||||||
<tr class="text-left" v-if="props.iterations.length > 0">
|
<tr class="text-left" v-if="props.iterations.length > 0">
|
||||||
<th>Époch</th>
|
<th class="sticky top-0 z-10 bg-background">Époch</th>
|
||||||
<th>Exemple</th>
|
<th class="sticky top-0 z-10 bg-background">Exemple</th>
|
||||||
<th
|
<th
|
||||||
v-for="(weight, index) in displayedWeights"
|
v-for="(weight, index) in displayedWeights"
|
||||||
v-bind:key="index"
|
v-bind:key="index"
|
||||||
|
class="sticky top-0 z-10 bg-background"
|
||||||
>
|
>
|
||||||
X<sub>{{ index }}</sub>
|
X<sub>{{ index }}</sub>
|
||||||
</th>
|
</th>
|
||||||
<th>Erreur</th>
|
<th class="sticky top-0 z-10 bg-background">Erreur</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
v-for="(iteration, index) in props.iterations"
|
v-for="(iteration, index) in props.iterations"
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ScrollAreaRootProps } from "reka-ui"
|
||||||
|
import { onBeforeUnmount, onMounted, ref, toRef, type HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit, useScroll } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
ScrollAreaCorner,
|
||||||
|
ScrollAreaRoot,
|
||||||
|
ScrollAreaViewport,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import ScrollBar from "./ScrollBar.vue"
|
||||||
|
|
||||||
|
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes["class"], keepBottom?: boolean }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const scrollAreaRootRef = ref<{ viewport: HTMLElement } | null>(null)
|
||||||
|
const { arrivedState } = useScroll(() => scrollAreaRootRef.value?.viewport)
|
||||||
|
const bottom = toRef(arrivedState, 'bottom')
|
||||||
|
const isFollowingBottom = ref(props.keepBottom ?? false)
|
||||||
|
let isAutoScrolling = false
|
||||||
|
let mutationObserver: MutationObserver | null = null
|
||||||
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
let followFrame: number | null = null
|
||||||
|
let previousScrollTop = 0
|
||||||
|
|
||||||
|
const getViewport = () => scrollAreaRootRef.value?.viewport
|
||||||
|
const isNearBottom = () => {
|
||||||
|
const viewport = getViewport()
|
||||||
|
|
||||||
|
return viewport ? viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight <= 4 : true
|
||||||
|
}
|
||||||
|
const handleScroll = () => {
|
||||||
|
const viewport = getViewport()
|
||||||
|
if (!viewport) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollTop = viewport.scrollTop
|
||||||
|
if (scrollTop < previousScrollTop) {
|
||||||
|
isAutoScrolling = false
|
||||||
|
isFollowingBottom.value = false
|
||||||
|
}
|
||||||
|
previousScrollTop = scrollTop
|
||||||
|
|
||||||
|
if (isAutoScrolling) {
|
||||||
|
if (isNearBottom()) {
|
||||||
|
isAutoScrolling = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isFollowingBottom.value = isNearBottom()
|
||||||
|
}
|
||||||
|
const handleUserScroll = () => {
|
||||||
|
isAutoScrolling = false
|
||||||
|
isFollowingBottom.value = false
|
||||||
|
const viewport = getViewport()
|
||||||
|
viewport?.scrollTo({ top: viewport.scrollTop, behavior: 'auto' })
|
||||||
|
}
|
||||||
|
const followBottomAfterUpdate = () => {
|
||||||
|
followFrame = null
|
||||||
|
if (isFollowingBottom.value) {
|
||||||
|
scrollToBottom('smooth')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const scheduleFollowBottom = () => {
|
||||||
|
if (followFrame === null) {
|
||||||
|
followFrame = requestAnimationFrame(followBottomAfterUpdate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBottom = () => bottom.value
|
||||||
|
const scrollToBottom = (behavior: string = 'auto') => {
|
||||||
|
const viewport = getViewport()
|
||||||
|
if (viewport) {
|
||||||
|
isFollowingBottom.value = true
|
||||||
|
isAutoScrolling = behavior === 'smooth'
|
||||||
|
previousScrollTop = viewport.scrollTop
|
||||||
|
viewport.scrollTo({
|
||||||
|
top: viewport.scrollHeight,
|
||||||
|
behavior: behavior === 'smooth' ? 'smooth' : 'auto',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const followBottom = (force = false) => {
|
||||||
|
if (!force && !isFollowingBottom.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isFollowingBottom.value = true
|
||||||
|
scrollToBottom('smooth')
|
||||||
|
}
|
||||||
|
const scrollToTop = (behavior: ScrollBehavior = 'smooth') => {
|
||||||
|
const viewport = getViewport()
|
||||||
|
if (viewport) {
|
||||||
|
isAutoScrolling = false
|
||||||
|
isFollowingBottom.value = false
|
||||||
|
viewport.scrollTo({ top: 0, behavior })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const viewport = getViewport()
|
||||||
|
if (!viewport) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewport.addEventListener('scroll', handleScroll, { passive: true })
|
||||||
|
viewport.addEventListener('wheel', handleUserScroll, { passive: true })
|
||||||
|
viewport.addEventListener('touchstart', handleUserScroll, { passive: true })
|
||||||
|
mutationObserver = new MutationObserver(() => {
|
||||||
|
if (props.keepBottom) {
|
||||||
|
scheduleFollowBottom()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
mutationObserver.observe(viewport, { childList: true, subtree: true })
|
||||||
|
resizeObserver = new ResizeObserver(scheduleFollowBottom)
|
||||||
|
resizeObserver.observe(viewport)
|
||||||
|
|
||||||
|
if (props.keepBottom) {
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
const viewport = getViewport()
|
||||||
|
viewport?.removeEventListener('scroll', handleScroll)
|
||||||
|
viewport?.removeEventListener('wheel', handleUserScroll)
|
||||||
|
viewport?.removeEventListener('touchstart', handleUserScroll)
|
||||||
|
mutationObserver?.disconnect()
|
||||||
|
resizeObserver?.disconnect()
|
||||||
|
if (followFrame !== null) {
|
||||||
|
cancelAnimationFrame(followFrame)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
interface Expose {
|
||||||
|
isBottom: () => boolean
|
||||||
|
isFollowingBottom: typeof isFollowingBottom
|
||||||
|
followBottom: (force?: boolean) => void
|
||||||
|
scrollToBottom: (behavior?: string) => void
|
||||||
|
scrollToTop: (behavior?: ScrollBehavior) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose<Expose>({
|
||||||
|
isBottom,
|
||||||
|
isFollowingBottom,
|
||||||
|
followBottom,
|
||||||
|
scrollToBottom,
|
||||||
|
scrollToTop,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ScrollAreaRoot
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('relative overflow-hidden', props.class)"
|
||||||
|
ref="scrollAreaRootRef"
|
||||||
|
>
|
||||||
|
<ScrollAreaViewport class="h-full w-full overflow-y-auto rounded-[inherit]">
|
||||||
|
<slot />
|
||||||
|
</ScrollAreaViewport>
|
||||||
|
<slot name="overlay" />
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaCorner />
|
||||||
|
</ScrollAreaRoot>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ScrollAreaScrollbarProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { ScrollAreaScrollbar, ScrollAreaThumb } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes["class"] }>(), {
|
||||||
|
orientation: "vertical",
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ScrollAreaScrollbar
|
||||||
|
data-slot="scroll-area-scrollbar"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="
|
||||||
|
cn('flex touch-none p-px transition-colors select-none',
|
||||||
|
orientation === 'vertical'
|
||||||
|
&& 'h-full w-2.5 border-l border-l-transparent',
|
||||||
|
orientation === 'horizontal'
|
||||||
|
&& 'h-2.5 flex-col border-t border-t-transparent',
|
||||||
|
props.class)"
|
||||||
|
>
|
||||||
|
<ScrollAreaThumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
class="bg-border relative flex-1 rounded-full"
|
||||||
|
/>
|
||||||
|
</ScrollAreaScrollbar>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as ScrollArea } from "./ScrollArea.vue"
|
||||||
|
export { default as ScrollBar } from "./ScrollBar.vue"
|
||||||
@@ -11,8 +11,11 @@ import {
|
|||||||
PointElement,
|
PointElement,
|
||||||
LineElement,
|
LineElement,
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import { computed, ref } from 'vue';
|
import { ArrowDown, ArrowUp } from 'lucide-vue-next';
|
||||||
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
import LinkHeader from '@/components/LinkHeader.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 { usePerceptronTraining } from '@/composables/usePerceptronTraining';
|
import { usePerceptronTraining } from '@/composables/usePerceptronTraining';
|
||||||
import type { Dataset, DatasetPoint, InitializationMethod, PerceptronType } from '@/types/perceptron';
|
import type { Dataset, DatasetPoint, InitializationMethod, PerceptronType } from '@/types/perceptron';
|
||||||
import IterationTable from '../components/IterationTable.vue';
|
import IterationTable from '../components/IterationTable.vue';
|
||||||
@@ -105,11 +108,22 @@ function getActivationFunction(type: string): (x: number) => number {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Perceptron Viewer"></Head>
|
<Head title="Perceptron Viewer"></Head>
|
||||||
<main class="space-y-6">
|
<main class="flex min-h-dvh flex-col gap-6 pb-6 space-y-6">
|
||||||
<LinkHeader class="w-full" />
|
<LinkHeader class="w-full" />
|
||||||
<PerceptronSetup
|
<PerceptronSetup
|
||||||
:type="props.type"
|
:type="props.type"
|
||||||
@@ -130,18 +144,56 @@ function getActivationFunction(type: string): (x: number) => number {
|
|||||||
@update:training-id="setTrainingId"
|
@update:training-id="setTrainingId"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="align-items-start justify-content-center flex h-full min-h-dvh max-w-dvw"
|
class="align-items-start justify-content-center grid max-w-dvw grid-cols-2"
|
||||||
v-if="selectedDatasetName || iterations.length > 0"
|
v-if="selectedDatasetName || iterations.length > 0"
|
||||||
>
|
>
|
||||||
<div class="max-h-full w-full overflow-y-scroll">
|
<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
|
<IterationTable
|
||||||
:iterations="iterations"
|
:iterations="iterations"
|
||||||
:trainingEnded="trainingEnded"
|
:trainingEnded="trainingEnded"
|
||||||
:trainingEndReason="trainingEndReason"
|
:trainingEndReason="trainingEndReason"
|
||||||
:maxDisplayedWeights="props.maxDisplayedWeights"
|
: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>
|
</div>
|
||||||
<div class="sticky top-0 h-full w-full">
|
</template>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
<div class="h-full w-full min-w-0 space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<PerceptronDecisionGraph
|
<PerceptronDecisionGraph
|
||||||
:cleanedDataset="cleanedDataset"
|
:cleanedDataset="cleanedDataset"
|
||||||
@@ -163,3 +215,16 @@ function getActivationFunction(type: string): (x: number) => number {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user