mirror of
https://github.com/ostris/ai-toolkit.git
synced 2026-02-01 19:39:46 +00:00
191 lines
6.0 KiB
TypeScript
191 lines
6.0 KiB
TypeScript
'use client';
|
|
import { useState, useEffect, useMemo } from 'react';
|
|
import { createGlobalState } from 'react-global-hooks';
|
|
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react';
|
|
|
|
export interface SampleImageModalState {
|
|
imgPath: string;
|
|
numSamples: number;
|
|
sampleImages: string[];
|
|
}
|
|
|
|
export const sampleImageModalState = createGlobalState<SampleImageModalState | null>(null);
|
|
|
|
export const openSampleImage = (sampleImageProps: SampleImageModalState) => {
|
|
sampleImageModalState.set(sampleImageProps);
|
|
};
|
|
|
|
export default function SampleImageModal() {
|
|
const [imageModal, setImageModal] = sampleImageModalState.use();
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (imageModal) {
|
|
setIsOpen(true);
|
|
}
|
|
}, [imageModal]);
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) {
|
|
// use timeout to allow the dialog to close before resetting the state
|
|
setTimeout(() => {
|
|
setImageModal(null);
|
|
}, 500);
|
|
}
|
|
}, [isOpen]);
|
|
|
|
const onCancel = () => {
|
|
setIsOpen(false);
|
|
};
|
|
|
|
const imgInfo = useMemo(() => {
|
|
const ii = {
|
|
filename: '',
|
|
step: 0,
|
|
promptIdx: 0,
|
|
};
|
|
if (imageModal?.imgPath) {
|
|
const filename = imageModal.imgPath.split('/').pop();
|
|
if (!filename) return ii;
|
|
// filename is <timestep>__<zero_pad_step>_<prompt_idx>.<ext>
|
|
ii.filename = filename as string;
|
|
const parts = filename
|
|
.split('.')[0]
|
|
.split('_')
|
|
.filter(p => p !== '');
|
|
if (parts.length === 3) {
|
|
ii.step = parseInt(parts[1]);
|
|
ii.promptIdx = parseInt(parts[2]);
|
|
}
|
|
}
|
|
return ii;
|
|
}, [imageModal]);
|
|
|
|
const handleArrowUp = () => {
|
|
if (!imageModal) return;
|
|
console.log('Arrow Up pressed');
|
|
// Change image to same sample but up one step
|
|
const currentIdx = imageModal.sampleImages.findIndex(img => img === imageModal.imgPath);
|
|
if (currentIdx === -1) return;
|
|
const nextIdx = currentIdx - imageModal.numSamples;
|
|
if (nextIdx < 0) return;
|
|
openSampleImage({
|
|
imgPath: imageModal.sampleImages[nextIdx],
|
|
numSamples: imageModal.numSamples,
|
|
sampleImages: imageModal.sampleImages,
|
|
});
|
|
};
|
|
|
|
const handleArrowDown = () => {
|
|
if (!imageModal) return;
|
|
console.log('Arrow Down pressed');
|
|
// Change image to same sample but down one step
|
|
const currentIdx = imageModal.sampleImages.findIndex(img => img === imageModal.imgPath);
|
|
if (currentIdx === -1) return;
|
|
const nextIdx = currentIdx + imageModal.numSamples;
|
|
if (nextIdx >= imageModal.sampleImages.length) return;
|
|
openSampleImage({
|
|
imgPath: imageModal.sampleImages[nextIdx],
|
|
numSamples: imageModal.numSamples,
|
|
sampleImages: imageModal.sampleImages,
|
|
});
|
|
};
|
|
|
|
const handleArrowLeft = () => {
|
|
if (!imageModal) return;
|
|
if (imgInfo.promptIdx === 0) return;
|
|
console.log('Arrow Left pressed');
|
|
// go to previous sample
|
|
const currentIdx = imageModal.sampleImages.findIndex(img => img === imageModal.imgPath);
|
|
if (currentIdx === -1) return;
|
|
const minIdx = currentIdx - imgInfo.promptIdx;
|
|
const nextIdx = currentIdx - 1;
|
|
if (nextIdx < minIdx) return;
|
|
openSampleImage({
|
|
imgPath: imageModal.sampleImages[nextIdx],
|
|
numSamples: imageModal.numSamples,
|
|
sampleImages: imageModal.sampleImages,
|
|
});
|
|
};
|
|
|
|
const handleArrowRight = () => {
|
|
if (!imageModal) return;
|
|
console.log('Arrow Right pressed');
|
|
// go to next sample
|
|
const currentIdx = imageModal.sampleImages.findIndex(img => img === imageModal.imgPath);
|
|
if (currentIdx === -1) return;
|
|
const stepMinIdx = currentIdx - imgInfo.promptIdx;
|
|
const maxIdx = stepMinIdx + imageModal.numSamples - 1;
|
|
const nextIdx = currentIdx + 1;
|
|
if (nextIdx > maxIdx) return;
|
|
if (nextIdx >= imageModal.sampleImages.length) return;
|
|
openSampleImage({
|
|
imgPath: imageModal.sampleImages[nextIdx],
|
|
numSamples: imageModal.numSamples,
|
|
sampleImages: imageModal.sampleImages,
|
|
});
|
|
};
|
|
|
|
// Handle keyboard events
|
|
useEffect(() => {
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (!isOpen) return;
|
|
|
|
switch (event.key) {
|
|
case 'Escape':
|
|
onCancel();
|
|
break;
|
|
case 'ArrowUp':
|
|
handleArrowUp();
|
|
break;
|
|
case 'ArrowDown':
|
|
handleArrowDown();
|
|
break;
|
|
case 'ArrowLeft':
|
|
handleArrowLeft();
|
|
break;
|
|
case 'ArrowRight':
|
|
handleArrowRight();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
|
return () => {
|
|
window.removeEventListener('keydown', handleKeyDown);
|
|
};
|
|
}, [isOpen, imageModal, imgInfo]);
|
|
|
|
return (
|
|
<Dialog open={isOpen} onClose={onCancel} className="relative z-10">
|
|
<DialogBackdrop
|
|
transition
|
|
className="fixed inset-0 bg-gray-900/75 transition-opacity data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in"
|
|
/>
|
|
|
|
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
|
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
|
<DialogPanel
|
|
transition
|
|
className="relative transform overflow-hidden rounded-lg bg-gray-800 text-left shadow-xl transition-all data-closed:translate-y-4 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in max-w-[95%] max-h-[95vh] data-closed:sm:translate-y-0 data-closed:sm:scale-95"
|
|
>
|
|
<div className="flex justify-center items-center">
|
|
{imageModal?.imgPath && (
|
|
<img
|
|
src={`/api/img/${encodeURIComponent(imageModal.imgPath)}`}
|
|
alt="Sample Image"
|
|
className="max-w-full max-h-[calc(95vh-2rem)] object-contain"
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="bg-gray-950 text-center text-sm p-2">step: {imgInfo.step}</div>
|
|
</DialogPanel>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
);
|
|
}
|