Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c78a1aa
Rework FSC Create and Refine GUI
horatiualmasan Oct 21, 2025
ec3d50b
format
horatiualmasan Oct 22, 2025
9e3dca9
update
horatiualmasan Oct 22, 2025
99b99bd
fix test and format
horatiualmasan Oct 22, 2025
2460728
clean-up
horatiualmasan Oct 22, 2025
bdeb209
format
horatiualmasan Oct 22, 2025
d311094
Merge branch 'main' into horatiu-lig-7767-implement-new-fsc-createedi…
horatiualmasan Oct 22, 2025
c552a37
add slider for dialogs
horatiualmasan Oct 23, 2025
d05c039
Merge branch 'horatiu-lig-7767-implement-new-fsc-createedit-wizard' o…
horatiualmasan Oct 23, 2025
25576a5
update menu
horatiualmasan Oct 23, 2025
d81145f
make instruction section expandable
horatiualmasan Oct 23, 2025
eb06df8
update resizing
horatiualmasan Oct 23, 2025
f3b2eb4
update based on feedback
horatiualmasan Oct 23, 2025
63a36a0
remove close button from nottifications
horatiualmasan Oct 23, 2025
796c7c9
Merge branch 'main' into horatiu-lig-7767-implement-new-fsc-createedi…
horatiualmasan Oct 23, 2025
98f7183
clean up
horatiualmasan Oct 23, 2025
af97365
Merge branch 'horatiu-lig-7767-implement-new-fsc-createedit-wizard' o…
horatiualmasan Oct 23, 2025
2b23fba
Merge branch 'main' into horatiu-lig-7767-implement-new-fsc-createedi…
horatiualmasan Oct 23, 2025
575c7cc
remove unneded flag
horatiualmasan Oct 24, 2025
71cfb65
Merge branch 'horatiu-lig-7767-implement-new-fsc-createedit-wizard' o…
horatiualmasan Oct 24, 2025
2a66e3c
remove unused import
horatiualmasan Oct 24, 2025
06014d3
fix issue with missing samples for classifier
horatiualmasan Oct 24, 2025
e0a5951
update after review
horatiualmasan Oct 24, 2025
be38bd8
update after review
horatiualmasan Oct 24, 2025
c29e8e7
fix tests
horatiualmasan Oct 24, 2025
8e35646
Merge branch 'main' into horatiu-lig-7767-implement-new-fsc-createedi…
horatiualmasan Oct 24, 2025
d141dc4
show classifier button only on sample page
horatiualmasan Oct 24, 2025
3fff160
Merge branch 'horatiu-lig-7767-implement-new-fsc-createedit-wizard' o…
horatiualmasan Oct 24, 2025
fe2dc51
fix
horatiualmasan Oct 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<script lang="ts">
import { SampleImage, SelectableBox } from '$lib/components';
import { useClassifierState } from '$lib/hooks/useClassifiers/useClassifierState';
import { useSamplesInfinite } from '$lib/hooks/useSamplesInfinite/useSamplesInfinite';
import { useSettings } from '$lib/hooks/useSettings';
import { Grid } from 'svelte-virtual';
import type { SampleView } from '$lib/api/lightly_studio_local';

const { dataset_id }: { dataset_id: string } = $props();

const { classifierSamples, classifierSelectedSampleIds, toggleClassifierSampleSelection } =
useClassifierState();
const { gridViewSampleRenderingStore } = useSettings();

const samplesParams = $derived({
dataset_id,
mode: 'classifier' as const,
classifierSamples: $classifierSamples || undefined
});

const { samples: infiniteSamples } = $derived(useSamplesInfinite(samplesParams));

const displayedSamples: SampleView[] = $derived(
$infiniteSamples &&
$infiniteSamples.data &&
$classifierSamples &&
($classifierSamples.positiveSampleIds.length > 0 ||
$classifierSamples.negativeSampleIds.length > 0)
? $infiniteSamples.data.pages.flatMap((page) => page.data)
: []
);

let viewport: HTMLElement | null = $state(null);
let objectFit = $state($gridViewSampleRenderingStore);
// Set initial height
let viewportHeight = $state(400);

// Grid configuration - 4 images per row
const sampleWidth = 160;
const sampleHeight = 160;
const GridGap = 6;

// Update viewport height when viewport changes
$effect(() => {
if (viewport) {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
viewportHeight = Math.max(entry.contentRect.height, 200);
}
});
resizeObserver.observe(viewport);
return () => resizeObserver.disconnect();
}
});

const handleOnClick: (event: MouseEvent & { currentTarget: HTMLElement }) => void = (event) => {
const sampleId = event.currentTarget.dataset.sampleId!;
toggleSampleSelection(sampleId);
};

const handleOnDoubleClick: (event: MouseEvent & { currentTarget: HTMLElement }) => void = (
event
) => {
event.preventDefault();
const sampleId = event.currentTarget.dataset.sampleId!;
toggleSampleSelection(sampleId);
};

const handleKeyDown: (event: KeyboardEvent & { currentTarget: HTMLElement }) => void = (
event
) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
const sampleId = event.currentTarget.dataset.sampleId!;
toggleSampleSelection(sampleId);
}
};

function toggleSampleSelection(sampleId: string) {
toggleClassifierSampleSelection(sampleId);
}
</script>

{#if $infiniteSamples.isPending}
<!-- Loading state -->
<div class="flex h-full w-full items-center justify-center">
<div class="text-sm text-muted-foreground">Loading samples...</div>
</div>
{:else if $infiniteSamples.isError}
<!-- Error state -->
<div class="flex h-full w-full items-center justify-center">
<div class="text-sm text-muted-foreground">Error loading samples</div>
</div>
{:else if $infiniteSamples.isSuccess && displayedSamples.length === 0}
<!-- Empty state -->
<div class="flex h-full w-full items-center justify-center">
<div class="text-center text-muted-foreground">
<div class="mb-2 text-sm font-medium">No samples available</div>
<div class="text-xs">No samples found for this classifier.</div>
</div>
</div>
{:else}
<!-- Main grid content -->
<div class="viewport h-full w-full" bind:this={viewport}>
{#if displayedSamples.length > 0}
<Grid
itemCount={displayedSamples.length}
itemHeight={sampleHeight + GridGap}
itemWidth={sampleWidth + GridGap}
height={viewportHeight}
class="overflow-none overflow-y-auto dark:[color-scheme:dark]"
style="--sample-width: {sampleWidth}px; --sample-height: {sampleHeight}px;"
overScan={5}
>
{#snippet item({ index, style }: { index: number; style: string })}
{#key $infiniteSamples.dataUpdatedAt}
{#if displayedSamples[index]}
<div
class="relative cursor-pointer"
class:sample-selected={$classifierSelectedSampleIds.has(
displayedSamples[index].sample_id
)}
{style}
data-testid="classifier-sample-grid-item"
data-sample-id={displayedSamples[index].sample_id}
data-sample-name={displayedSamples[index].file_name}
data-index={index}
onclick={handleOnClick}
ondblclick={handleOnDoubleClick}
onkeydown={handleKeyDown}
aria-label={`Select sample: ${displayedSamples[index].file_name}`}
role="button"
tabindex="0"
>
<div class="absolute inset-0 z-10">
<SelectableBox
onSelect={() => undefined}
isSelected={$classifierSelectedSampleIds.has(
displayedSamples[index].sample_id
)}
/>
</div>

<SampleImage sample={displayedSamples[index]} {objectFit} />
</div>
{/if}
{/key}
{/snippet}
</Grid>
{:else}
<div class="flex h-full w-full items-center justify-center">
<div class="text-sm text-muted-foreground">No samples to display</div>
</div>
{/if}
</div>
{/if}

<style>
.viewport {
overflow-y: hidden;
}

.sample-selected {
outline: drop-shadow(1px 1px 1px hsl(var(--primary)))
drop-shadow(1px -1px 1px hsl(var(--primary)))
drop-shadow(-1px -1px 1px hsl(var(--primary)))
drop-shadow(-1px 1px 1px hsl(var(--primary)));
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import { useCreateClassifiersPanel } from '$lib/hooks/useClassifiers/useCreateClassifiersPanel';
import { useRefineClassifiersPanel } from '$lib/hooks/useClassifiers/useRefineClassifiersPanel';
import { useClassifiers } from '$lib/hooks/useClassifiers/useClassifiers';
import { useClassifiersMenu } from '$lib/hooks/useClassifiers/useClassifiersMenu';
import { useQueryClient } from '@tanstack/svelte-query';
import {
readAnnotationLabelsOptions,
Expand All @@ -35,6 +36,13 @@

const { isCreateClassifiersPanelOpen } = useCreateClassifiersPanel();
const { isRefineClassifiersPanelOpen } = useRefineClassifiersPanel();
const {
isDropdownOpen,
activeTab,
switchToManageTab,
closeClassifiersMenu,
scrollToClassifierId
} = useClassifiersMenu();

// Classifier hook
const {
Expand All @@ -47,16 +55,15 @@
saveClassifier,
loadClassifier,
startRefinment,
startCreateClassifier
startCreateClassifier,
clearClassifiersSelected
} = useClassifiers();
const { selectedSampleIds } = useGlobalStorage();

// Store-based state
const exportType = writable<ClassifierExportType>('sklearn');
const showExportDialog = writable(false);
const selectedClassifierId = writable<string | null>(null);
const isDropdownOpen = writable(false);
const activeTab = writable('create');

// Derived stores
const isApplyButtonEnabled = derived(
Expand All @@ -66,6 +73,11 @@

const triggerContent = derived(exportType, ($type) => $type || 'Select export type');

// Sort classifiers alphabetically by name
const sortedClassifiers = derived(classifiers, ($classifiers) => {
return [...$classifiers].sort((a, b) => a.classifier_name.localeCompare(b.classifier_name));
});

// Handlers
function handleDownload(classifierId: string) {
selectedClassifierId.set(classifierId);
Expand Down Expand Up @@ -98,20 +110,43 @@
}

function handleNewClassifier() {
startCreateClassifier();
isDropdownOpen.set(false);
startCreateClassifier(new Event('click'));
closeClassifiersMenu();
}

function handleLoadClassifier(event: Event) {
loadClassifier(event);
// Switch to manage tab after successful load
activeTab.set('manage');
switchToManageTab();
}

// Close dropdown when panels open
// Close dropdown when wizard open
$effect(() => {
if ($isCreateClassifiersPanelOpen || $isRefineClassifiersPanelOpen) {
isDropdownOpen.set(false);
closeClassifiersMenu();
}
});

// Handle scrolling to and selecting a classifier
$effect(() => {
const classifierId = $scrollToClassifierId;
if (classifierId) {
// Use a small delay to ensure the DOM is updated
setTimeout(() => {
const element = document.querySelector(`[data-classifier-id="${classifierId}"]`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Select the classifier
classifierSelectionToggle(classifierId);
}
}, 50);
}
});

// Clear selection when dropdown is closed
$effect(() => {
if (!$isDropdownOpen) {
clearClassifiersSelected();
}
});
</script>
Expand All @@ -136,11 +171,6 @@
<div class="border-b">
<div class="p-4 pb-0">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-muted-foreground">
Create, manage, and run classifiers
</p>
</div>
<span
class="inline-flex items-center rounded-full bg-secondary px-2 py-1 text-xs font-medium text-secondary-foreground"
>
Expand Down Expand Up @@ -170,41 +200,37 @@
<div class="space-y-4">
<!-- Create New Classifier -->
<div class="space-y-3">
<div class="flex items-center gap-2">
<h4 class="text-sm font-medium">Create New Classifier</h4>
<Tooltip content="Create a new classifier from selected samples">
<Info class="size-4 text-muted-foreground" />
</Tooltip>
</div>
<Button
variant="default"
class="w-full"
onclick={handleNewClassifier}
disabled={$selectedSampleIds.size === 0}
>
<NetworkIcon class="mr-2 size-4" />
Create New Classifier
</Button>
{#if $selectedSampleIds.size === 0}
<p class="flex items-center gap-2 text-sm text-muted-foreground">
<Info class="size-4" />
Select samples to create a classifier
</p>
<div class="flex items-center gap-2">
<p class="flex items-center gap-2 text-sm text-orange-600">
<Info class="size-4" />
Select samples to create a classifier
</p>
</div>
{:else}
<p class="flex items-center gap-2 text-sm text-green-600">
<Info class="size-4" />
{$selectedSampleIds.size} samples selected
</p>
<Button
variant="default"
class="w-full"
onclick={handleNewClassifier}
disabled={$selectedSampleIds.size === 0}
>
<NetworkIcon class="mr-2 size-4" />
Create New Classifier
</Button>
{/if}
</div>

<!-- Separator -->
<div class="border-t border-border"></div>

<!-- Load Classifier -->
<div class="space-y-3">
<div class="flex items-center gap-2">
<h4 class="text-sm font-medium">Load Existing Classifier</h4>
<Tooltip content="Upload a previously saved classifier file">
<Info class="size-4 text-muted-foreground" />
</Tooltip>
</div>
<div class="relative">
<input
Expand All @@ -225,16 +251,18 @@

<!-- Manage & Run Tab -->
<TabsContent value="manage" class="space-y-4 px-4 pb-4">
{#if $classifiers.length > 0}
{#if $sortedClassifiers.length > 0}
<!-- Classifiers List -->
<div class="max-h-48 space-y-2 overflow-y-auto dark:[color-scheme:dark]">
{#each $classifiers as classifier (classifier.classifier_id)}
{#each $sortedClassifiers as classifier (classifier.classifier_id)}
<div
class="flex items-center justify-between rounded-lg border p-3 transition-colors hover:bg-muted/50"
data-classifier-id={classifier.classifier_id}
>
<div class="flex min-w-0 flex-1 items-center gap-3">
<Checkbox
name={classifier.classifier_id}
label=""
isChecked={$classifiersSelected.has(
classifier.classifier_id
)}
Expand All @@ -260,7 +288,7 @@
classifier.class_list,
datasetId
);
isDropdownOpen.set(false);
closeClassifiersMenu();
}}
>
<Pencil class="size-4" />
Expand Down
Loading