From 2e9de5eb507439217b5bdaba116c53a6db6067a5 Mon Sep 17 00:00:00 2001 From: Jaret Burkett Date: Mon, 29 Sep 2025 04:49:32 -0600 Subject: [PATCH] Add ability to delete samples from the ui --- ui/src/app/api/img/delete/route.ts | 11 ++++- ui/src/components/JobActionBar.tsx | 2 +- ui/src/components/SampleImageCard.tsx | 4 -- ui/src/components/SampleImageViewer.tsx | 56 ++++++++++++++++++++++++- ui/src/components/SampleImages.tsx | 18 +++++++- 5 files changed, 81 insertions(+), 10 deletions(-) diff --git a/ui/src/app/api/img/delete/route.ts b/ui/src/app/api/img/delete/route.ts index d4d968f8..d213c1c6 100644 --- a/ui/src/app/api/img/delete/route.ts +++ b/ui/src/app/api/img/delete/route.ts @@ -1,17 +1,24 @@ import { NextResponse } from 'next/server'; import fs from 'fs'; -import { getDatasetsRoot } from '@/server/settings'; +import { getDatasetsRoot, getTrainingFolder } from '@/server/settings'; export async function POST(request: Request) { try { const body = await request.json(); const { imgPath } = body; let datasetsPath = await getDatasetsRoot(); + const trainingPath = await getTrainingFolder(); + // make sure the dataset path is in the image path - if (!imgPath.startsWith(datasetsPath)) { + if (!imgPath.startsWith(datasetsPath) && !imgPath.startsWith(trainingPath)) { return NextResponse.json({ error: 'Invalid image path' }, { status: 400 }); } + // make sure it is an image + if (!/\.(jpg|jpeg|png|bmp|gif|tiff|webp)$/i.test(imgPath.toLowerCase())) { + return NextResponse.json({ error: 'Not an image' }, { status: 400 }); + } + // if img doesnt exist, ignore if (!fs.existsSync(imgPath)) { return NextResponse.json({ success: true }); diff --git a/ui/src/components/JobActionBar.tsx b/ui/src/components/JobActionBar.tsx index 0106a393..c6500c97 100644 --- a/ui/src/components/JobActionBar.tsx +++ b/ui/src/components/JobActionBar.tsx @@ -1,5 +1,5 @@ import Link from 'next/link'; -import { Eye, Trash2, Pen, Play, Pause, Cog, NotebookPen } from 'lucide-react'; +import { Eye, Trash2, Pen, Play, Pause, Cog } from 'lucide-react'; import { Button } from '@headlessui/react'; import { openConfirm } from '@/components/ConfirmModal'; import { Job } from '@prisma/client'; diff --git a/ui/src/components/SampleImageCard.tsx b/ui/src/components/SampleImageCard.tsx index d5234114..7f01bc8f 100644 --- a/ui/src/components/SampleImageCard.tsx +++ b/ui/src/components/SampleImageCard.tsx @@ -68,10 +68,6 @@ const SampleImageCard: React.FC = ({ } }, [isVisible, imageUrl]); - useEffect(() => { - console.log('SampleImageCard isVisible', isVisible, imageUrl); - }, [isVisible, imageUrl]); - const handleLoad = () => setLoaded(true); return ( diff --git a/ui/src/components/SampleImageViewer.tsx b/ui/src/components/SampleImageViewer.tsx index 49c7f9ee..b5fa1b01 100644 --- a/ui/src/components/SampleImageViewer.tsx +++ b/ui/src/components/SampleImageViewer.tsx @@ -3,6 +3,10 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'; import { SampleConfig, SampleItem } from '@/types'; +import { Cog } from 'lucide-react'; +import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'; +import { openConfirm } from './ConfirmModal'; +import { apiClient } from '@/utils/api'; interface Props { imgPath: string | null; // current image path @@ -10,9 +14,17 @@ interface Props { sampleImages: string[]; // all sample images sampleConfig: SampleConfig | null; onChange: (nextPath: string | null) => void; // parent setter + refreshSampleImages?: () => void; } -export default function SampleImageViewer({ imgPath, numSamples, sampleImages, sampleConfig, onChange }: Props) { +export default function SampleImageViewer({ + imgPath, + numSamples, + sampleImages, + sampleConfig, + onChange, + refreshSampleImages, +}: Props) { const [mounted, setMounted] = useState(false); const [isOpen, setIsOpen] = useState(Boolean(imgPath)); @@ -220,6 +232,48 @@ export default function SampleImageViewer({ imgPath, numSamples, sampleImages, s +
+ + + + + + +
{ + let message = `Are you sure you want to delete this sample? This action cannot be undone.`; + openConfirm({ + title: 'Delete Sample', + message: message, + type: 'warning', + confirmText: 'Delete', + onConfirm: () => { + apiClient + .post('/api/img/delete', { imgPath: imgPath }) + .then(() => { + console.log('Image deleted:', imgPath); + onChange(null); + if (refreshSampleImages) { + refreshSampleImages(); + } + }) + .catch(error => { + console.error('Error deleting image:', error); + }); + }, + }); + }} + > + Delete Sample +
+
+
+
+
diff --git a/ui/src/components/SampleImages.tsx b/ui/src/components/SampleImages.tsx index 50d0dead..b55d42eb 100644 --- a/ui/src/components/SampleImages.tsx +++ b/ui/src/components/SampleImages.tsx @@ -8,7 +8,7 @@ import { Button } from '@headlessui/react'; import { FaDownload } from 'react-icons/fa'; import { apiClient } from '@/utils/api'; import classNames from 'classnames'; -import { FaCaretDown } from 'react-icons/fa'; +import { FaCaretDown, FaCaretUp } from 'react-icons/fa'; import SampleImageViewer from './SampleImageViewer'; interface SampleImagesMenuProps { @@ -88,6 +88,12 @@ export default function SampleImages({ job }: SampleImagesProps) { } }; + const scrollToTop = () => { + if (containerRef.current) { + containerRef.current.scrollTo({ top: 0, behavior: 'instant' }); + } + }; + const PageInfoContent = useMemo(() => { let icon = null; let text = ''; @@ -276,9 +282,17 @@ export default function SampleImages({ job }: SampleImagesProps) { sampleImages={sampleImages} onChange={setPath => setSelectedSamplePath(setPath)} sampleConfig={sampleConfig} + refreshSampleImages={refreshSampleImages} />
+ +
+