Updates to handle video in a dataset on ui

This commit is contained in:
Jaret Burkett
2025-03-26 12:15:28 -06:00
parent 4595965e06
commit e4526ad4a4
13 changed files with 228 additions and 58 deletions

View File

@@ -75,7 +75,8 @@ export default function AddImagesModal() {
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.bmp'],
'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'],
'video/*': ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.m4v', '.flv'],
'text/*': ['.txt'],
},
multiple: true,

View File

@@ -1,15 +1,21 @@
'use client';
import { useRef } from 'react';
import { useState, useEffect } from 'react';
import { createGlobalState } from 'react-global-hooks';
import { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react';
import { FaExclamationTriangle, FaInfo } from 'react-icons/fa';
import { TextInput } from './formInputs';
import React from 'react';
import { useFromNull } from '@/hooks/useFromNull';
import classNames from 'classnames';
export interface ConfirmState {
title: string;
message?: string;
confirmText?: string;
type?: 'danger' | 'warning' | 'info';
onConfirm?: () => void;
inputTitle?: string;
onConfirm?: (value?: string) => void | Promise<void>;
onCancel?: () => void;
}
@@ -22,10 +28,21 @@ export const openConfirm = (confirmProps: ConfirmState) => {
export default function ConfirmModal() {
const [confirm, setConfirm] = confirmstate.use();
const [isOpen, setIsOpen] = useState(false);
const [inputValue, setInputValue] = useState<string>('');
const inputRef = useRef<HTMLInputElement>(null);
useFromNull(() => {
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, 100);
}, [confirm]);
useEffect(() => {
if (confirm) {
setIsOpen(true);
setInputValue('');
}
}, [confirm]);
@@ -47,7 +64,7 @@ export default function ConfirmModal() {
const onConfirm = () => {
if (confirm?.onConfirm) {
confirm.onConfirm();
confirm.onConfirm(inputValue);
}
setIsOpen(false);
};
@@ -136,12 +153,25 @@ export default function ConfirmModal() {
>
<Icon aria-hidden="true" className={`size-6 ${getTextColor()}`} />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left flex-1">
<DialogTitle as="h3" className={`text-base font-semibold ${getTitleColor()}`}>
{confirm?.title}
</DialogTitle>
<div className="mt-2">
<p className="text-sm text-gray-200">{confirm?.message}</p>
<div className={classNames('mt-4 w-full', { hidden: !confirm?.inputTitle })}>
<form onSubmit={(e) => {
e.preventDefault()
onConfirm()
}}>
<TextInput
value={inputValue}
ref={inputRef}
onChange={setInputValue}
placeholder={confirm?.inputTitle}
/>
</form>
</div>
</div>
</div>
</div>

View File

@@ -1,8 +1,9 @@
import React, { useRef, useEffect, useState, ReactNode, KeyboardEvent } from 'react';
import { FaTrashAlt } from 'react-icons/fa';
import { FaTrashAlt, FaEye, FaEyeSlash } from 'react-icons/fa';
import { openConfirm } from './ConfirmModal';
import classNames from 'classnames';
import { apiClient } from '@/utils/api';
import { isVideo } from '@/utils/basic';
interface DatasetImageCardProps {
imageUrl: string;
@@ -21,6 +22,7 @@ const DatasetImageCard: React.FC<DatasetImageCardProps> = ({
}) => {
const cardRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState<boolean>(false);
const [inViewport, setInViewport] = useState<boolean>(false);
const [loaded, setLoaded] = useState<boolean>(false);
const [isCaptionLoaded, setIsCaptionLoaded] = useState<boolean>(false);
const [caption, setCaption] = useState<string>('');
@@ -63,17 +65,25 @@ const DatasetImageCard: React.FC<DatasetImageCardProps> = ({
});
};
// Only fetch caption when the component is both in viewport and visible
useEffect(() => {
isVisible && fetchCaption();
}, [isVisible]);
if (inViewport && isVisible) {
fetchCaption();
}
}, [inViewport, isVisible]);
useEffect(() => {
// Create intersection observer to check visibility
// Create intersection observer to check viewport visibility
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) {
setIsVisible(true);
observer.disconnect();
setInViewport(true);
// Initialize isVisible to true when first coming into view
if (!isVisible) {
setIsVisible(true);
}
} else {
setInViewport(false);
}
},
{ threshold: 0.1 },
@@ -88,6 +98,13 @@ const DatasetImageCard: React.FC<DatasetImageCardProps> = ({
};
}, []);
const toggleVisibility = (): void => {
setIsVisible(prev => !prev);
if (!isVisible && !isCaptionLoaded) {
fetchCaption();
}
};
const handleLoad = (): void => {
setLoaded(true);
};
@@ -102,6 +119,8 @@ const DatasetImageCard: React.FC<DatasetImageCardProps> = ({
const isCaptionCurrent = caption.trim() === savedCaption;
const isItAVideo = isVideo(imageUrl);
return (
<div className={`flex flex-col ${className}`}>
{/* Square image container */}
@@ -111,24 +130,43 @@ const DatasetImageCard: React.FC<DatasetImageCardProps> = ({
style={{ paddingBottom: '100%' }} // Make it square
>
<div className="absolute inset-0 rounded-t-lg shadow-md">
{isVisible && (
<img
src={`/api/img/${encodeURIComponent(imageUrl)}`}
alt={alt}
onLoad={handleLoad}
className={`w-full h-full object-contain transition-opacity duration-300 ${
loaded ? 'opacity-100' : 'opacity-0'
}`}
/>
{inViewport && isVisible && (
<>
{isItAVideo ? (
<video
src={`/api/img/${encodeURIComponent(imageUrl)}`}
className={`w-full h-full object-contain`}
autoPlay={false}
loop
muted
controls
/>
) : (
<img
src={`/api/img/${encodeURIComponent(imageUrl)}`}
alt={alt}
onLoad={handleLoad}
className={`w-full h-full object-contain transition-opacity duration-300 ${
loaded ? 'opacity-100' : 'opacity-0'
}`}
/>
)}
</>
)}
{!isVisible && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-800 bg-opacity-75 rounded-t-lg">
<span className="text-white text-lg"></span>
</div>
)}
{children && <div className="absolute inset-0 flex items-center justify-center">{children}</div>}
<div className="absolute top-1 right-1">
<div className="absolute top-1 right-1 flex space-x-2">
<button
className="bg-gray-800 rounded-full p-2"
onClick={() => {
openConfirm({
title: 'Delete Image',
message: 'Are you sure you want to delete this image? This action cannot be undone.',
title: `Delete ${isItAVideo ? 'video' : 'image'}`,
message: `Are you sure you want to delete this ${isItAVideo ? 'video' : 'image'}? This action cannot be undone.`,
type: 'warning',
confirmText: 'Delete',
onConfirm: () => {
@@ -158,7 +196,7 @@ const DatasetImageCard: React.FC<DatasetImageCardProps> = ({
'border-transparent border-2': isCaptionCurrent,
})}
>
{isVisible && isCaptionLoaded && (
{inViewport && isVisible && isCaptionLoaded && (
<form
onSubmit={e => {
e.preventDefault();
@@ -175,9 +213,19 @@ const DatasetImageCard: React.FC<DatasetImageCardProps> = ({
/>
</form>
)}
{(!inViewport || !isVisible) && isCaptionLoaded && (
<div className="w-full h-full flex items-center justify-center text-gray-400">
{isVisible ? "Scroll into view to edit caption" : "Show content to edit caption"}
</div>
)}
{!isCaptionLoaded && (
<div className="w-full h-full flex items-center justify-center text-gray-400">
Loading caption...
</div>
)}
</div>
</div>
);
};
export default DatasetImageCard;
export default DatasetImageCard;

View File

@@ -1,5 +1,6 @@
import React, { useRef, useEffect, useState, ReactNode } from 'react';
import { sampleImageModalState } from '@/components/SampleImageModal';
import { isVideo } from '@/utils/basic';
interface SampleImageCardProps {
imageUrl: string;
@@ -47,6 +48,7 @@ const SampleImageCard: React.FC<SampleImageCardProps> = ({
const handleLoad = (): void => {
setLoaded(true);
};
console.log('imgurl',imageUrl.toLowerCase().slice(-4))
return (
<div className={`flex flex-col ${className}`}>
@@ -59,14 +61,27 @@ const SampleImageCard: React.FC<SampleImageCardProps> = ({
>
<div className="absolute inset-0 rounded-t-lg shadow-md">
{isVisible && (
<img
src={`/api/img/${encodeURIComponent(imageUrl)}`}
alt={alt}
onLoad={handleLoad}
className={`w-full h-full object-contain transition-opacity duration-300 ${
loaded ? 'opacity-100' : 'opacity-0'
}`}
/>
<>
{isVideo(imageUrl) ? (
<video
src={`/api/img/${encodeURIComponent(imageUrl)}`}
className={`w-full h-full object-cover`}
autoPlay={false}
loop
muted
playsInline
/>
) : (
<img
src={`/api/img/${encodeURIComponent(imageUrl)}`}
alt={alt}
onLoad={handleLoad}
className={`w-full h-full object-cover transition-opacity duration-300 ${
loaded ? 'opacity-100' : 'opacity-0'
}`}
/>
)}
</>
)}
{children && <div className="absolute inset-0 flex items-center justify-center">{children}</div>}
</div>

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { forwardRef } from 'react';
import classNames from 'classnames';
const labelClasses = 'block text-xs mb-1 mt-2 text-gray-300';
@@ -19,26 +19,30 @@ export interface TextInputProps extends InputProps {
disabled?: boolean;
}
export const TextInput = (props: TextInputProps) => {
const { label, value, onChange, placeholder, required, disabled } = props;
return (
<div className={classNames(props.className)}>
{label && <label className={labelClasses}>{label}</label>}
<input
type={props.type || 'text'}
value={value}
onChange={e => {
if (disabled) return;
onChange(e.target.value);
}}
className={`${inputClasses} ${disabled && 'opacity-30 cursor-not-allowed'}`}
placeholder={placeholder}
required={required}
disabled={disabled}
/>
</div>
);
};
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ label, value, onChange, placeholder, required, disabled, type = 'text', className }, ref) => {
return (
<div className={classNames(className)}>
{label && <label className={labelClasses}>{label}</label>}
<input
ref={ref}
type={type}
value={value}
onChange={e => {
if (!disabled) onChange(e.target.value);
}}
className={`${inputClasses} ${disabled ? 'opacity-30 cursor-not-allowed' : ''}`}
placeholder={placeholder}
required={required}
disabled={disabled}
/>
</div>
);
}
);
// 👇 Helpful for debugging
TextInput.displayName = 'TextInput';
export interface NumberInputProps extends InputProps {
value: number;