You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

223 lines
7.5 KiB

import React, { useState, useEffect, useCallback } from 'react';
const FONT_SIZE_STORAGE_KEY = 'prompt-editor-font-size';
const DEFAULT_FONT_SIZE = 14;
const MIN_FONT_SIZE = 10;
const MAX_FONT_SIZE = 24;
const FONT_SIZE_OPTIONS = [10, 12, 14, 16, 18, 20, 24];
interface PromptEditorModalProps {
isOpen: boolean;
initialPrompt: string;
onSubmit: (prompt: string) => void;
onClose: () => void;
}
export const PromptEditorModal: React.FC<PromptEditorModalProps> = ({
isOpen,
initialPrompt,
onSubmit,
onClose,
}) => {
const [prompt, setPrompt] = useState(initialPrompt);
const [showConfirmation, setShowConfirmation] = useState(false);
const [fontSize, setFontSize] = useState(() => {
// Load font size from localStorage on mount
if (typeof window !== 'undefined') {
const saved = localStorage.getItem(FONT_SIZE_STORAGE_KEY);
if (saved) {
const parsed = parseInt(saved, 10);
if (!isNaN(parsed) && parsed >= MIN_FONT_SIZE && parsed <= MAX_FONT_SIZE) {
return parsed;
}
}
}
return DEFAULT_FONT_SIZE;
});
// Update local state when initial prompt changes
useEffect(() => {
setPrompt(initialPrompt);
}, [initialPrompt]);
// Save font size to localStorage when it changes
useEffect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem(FONT_SIZE_STORAGE_KEY, fontSize.toString());
}
}, [fontSize]);
// Track unsaved changes
const hasUnsavedChanges = prompt !== initialPrompt;
// Handle close attempt - show confirmation if there are unsaved changes
const handleAttemptClose = useCallback(() => {
if (hasUnsavedChanges) {
setShowConfirmation(true);
} else {
onClose();
}
}, [hasUnsavedChanges, onClose]);
// Handle Escape key to close
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
handleAttemptClose();
}
};
if (isOpen) {
window.addEventListener('keydown', handleKeyDown);
}
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, handleAttemptClose]);
const handleSubmit = useCallback(() => {
onSubmit(prompt);
onClose();
}, [prompt, onSubmit, onClose]);
const handleFontSizeChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
setFontSize(parseInt(e.target.value, 10));
}, []);
const handleBackdropClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
// Only close if clicking the backdrop itself, not the dialog content
if (e.target === e.currentTarget) {
handleAttemptClose();
}
},
[handleAttemptClose]
);
const handleDismissConfirmation = useCallback(() => {
setShowConfirmation(false);
}, []);
const handleConfirmationBackdropClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
// Only dismiss if clicking the backdrop itself, not the confirmation dialog
if (e.target === e.currentTarget) {
handleDismissConfirmation();
}
},
[handleDismissConfirmation]
);
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50"
onClick={handleBackdropClick}
>
<div className="relative bg-neutral-800 border border-neutral-700 rounded-lg shadow-2xl w-full max-w-3xl h-[85vh] flex flex-col mx-4">
{/* Header */}
<div className="px-6 pt-6 pb-4">
<h2 className="text-xl font-semibold text-neutral-100">
Edit Prompt
</h2>
</div>
{/* Box containing toolbar and textarea */}
<div className="mx-6 flex-1 flex flex-col border border-neutral-700 rounded bg-neutral-900/30 overflow-hidden mb-4">
{/* Toolbar - header of the box */}
<div className="h-12 bg-neutral-900 border-b border-neutral-700 flex items-center px-4 gap-3 shrink-0">
{/* Font Size Control */}
<select
value={fontSize}
onChange={handleFontSizeChange}
className="text-sm py-1 px-2 border border-neutral-700 rounded bg-neutral-900/50 focus:outline-none focus:ring-1 focus:ring-neutral-600 text-neutral-300"
>
{FONT_SIZE_OPTIONS.map((size) => (
<option key={size} value={size}>
{size}px
</option>
))}
</select>
</div>
{/* Textarea */}
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe what to generate..."
className="nodrag nopan nowheel flex-1 w-full p-6 leading-relaxed text-neutral-100 bg-transparent border-0 resize-none focus:outline-none placeholder:text-neutral-500"
style={{ fontSize: `${fontSize}px` }}
autoFocus
/>
</div>
{/* Footer with buttons */}
<div className="flex justify-end gap-3 px-6 pb-6">
<button
onClick={handleAttemptClose}
className="px-4 py-2 text-sm font-medium text-neutral-300 bg-neutral-700 hover:bg-neutral-600 rounded transition-colors focus:outline-none focus:ring-1 focus:ring-neutral-500"
>
Cancel
</button>
<button
onClick={handleSubmit}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-500 rounded transition-colors focus:outline-none focus:ring-1 focus:ring-blue-400"
>
Submit
</button>
</div>
{/* Confirmation overlay */}
{showConfirmation && (
<div
className="absolute inset-0 flex items-center justify-center bg-black/60 rounded-lg"
onClick={handleConfirmationBackdropClick}
>
<div className="relative bg-neutral-800 border border-neutral-600 rounded-lg p-6 mx-4 max-w-sm shadow-xl">
{/* Close button */}
<button
onClick={handleDismissConfirmation}
className="absolute top-3 right-3 text-neutral-400 hover:text-neutral-200 transition-colors focus:outline-none"
aria-label="Close"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<p className="text-neutral-100 text-center mb-6">
You have unsaved changes
</p>
<div className="flex justify-center gap-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-neutral-300 bg-neutral-700 hover:bg-neutral-600 rounded transition-colors focus:outline-none focus:ring-1 focus:ring-neutral-500"
>
Discard
</button>
<button
onClick={handleSubmit}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-500 rounded transition-colors focus:outline-none focus:ring-1 focus:ring-blue-400"
>
Submit
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
};