From f778d979b59cbf3d8641f60d75a80c02ea690a72 Mon Sep 17 00:00:00 2001 From: Jaret Burkett Date: Thu, 20 Feb 2025 16:17:00 -0700 Subject: [PATCH] Saving captions is working --- ui/src/app/api/img/caption/route.ts | 30 +++++++++++++ ui/src/components/DatasetImageCard.tsx | 58 ++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 ui/src/app/api/img/caption/route.ts diff --git a/ui/src/app/api/img/caption/route.ts b/ui/src/app/api/img/caption/route.ts new file mode 100644 index 00000000..d2b22c68 --- /dev/null +++ b/ui/src/app/api/img/caption/route.ts @@ -0,0 +1,30 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import { getDatasetsRoot } from '@/app/api/datasets/utils'; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { imgPath, caption } = body; + let datasetsPath = await getDatasetsRoot(); + // make sure the dataset path is in the image path + if (!imgPath.startsWith(datasetsPath)) { + return NextResponse.json({ error: 'Invalid image path' }, { status: 400 }); + } + + // if img doesnt exist, ignore + if (!fs.existsSync(imgPath)) { + return NextResponse.json({ error: 'Image does not exist' }, { status: 404 }); + } + + + // check for caption + const captionPath = imgPath.replace(/\.[^/.]+$/, '') + '.txt'; + // save caption to file + fs.writeFileSync(captionPath, caption); + + return NextResponse.json({ success: true }); + } catch (error) { + return NextResponse.json({ error: 'Failed to create dataset' }, { status: 500 }); + } +} diff --git a/ui/src/components/DatasetImageCard.tsx b/ui/src/components/DatasetImageCard.tsx index b597ba08..0dd2e243 100644 --- a/ui/src/components/DatasetImageCard.tsx +++ b/ui/src/components/DatasetImageCard.tsx @@ -1,6 +1,7 @@ -import React, { useRef, useEffect, useState, ReactNode } from 'react'; +import React, { useRef, useEffect, useState, ReactNode, KeyboardEvent } from 'react'; import { FaTrashAlt } from 'react-icons/fa'; import { openConfirm } from './ConfirmModal'; +import classNames from 'classnames'; interface DatasetImageCardProps { imageUrl: string; @@ -22,6 +23,7 @@ const DatasetImageCard: React.FC = ({ const [loaded, setLoaded] = useState(false); const [isCaptionLoaded, setIsCaptionLoaded] = useState(false); const [caption, setCaption] = useState(''); + const [savedCaption, setSavedCaption] = useState(''); const isGettingCaption = useRef(false); const fetchCaption = async () => { @@ -31,12 +33,33 @@ const DatasetImageCard: React.FC = ({ const response = await fetch(`/api/caption/${encodeURIComponent(imageUrl)}`); const data = await response.text(); setCaption(data); + setSavedCaption(data); setIsCaptionLoaded(true); } catch (error) { console.error('Error fetching caption:', error); } }; + const saveCaption = () => { + const trimmedCaption = caption.trim(); + if (trimmedCaption === savedCaption) return; + fetch('/api/img/caption', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ imgPath: imageUrl, caption: trimmedCaption }), + }) + .then(res => res.json()) + .then(data => { + console.log('Caption saved:', data); + setSavedCaption(trimmedCaption); + }) + .catch(error => { + console.error('Error saving caption:', error); + }); + }; + useEffect(() => { isVisible && fetchCaption(); }, [isVisible]); @@ -66,6 +89,16 @@ const DatasetImageCard: React.FC = ({ setLoaded(true); }; + const handleKeyDown = (e: KeyboardEvent): void => { + // If Enter is pressed without Shift, prevent default behavior and save + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + saveCaption(); + } + }; + + const isCaptionCurrent = caption.trim() === savedCaption; + return (
{/* Square image container */} @@ -122,10 +155,27 @@ const DatasetImageCard: React.FC = ({
{/* Text area below the image */} -
+
{isVisible && isCaptionLoaded && ( -
-