|
|
import { useState, useRef, useEffect } from "react"; |
|
|
import { PROMPTS, THEME } from "../constants"; |
|
|
|
|
|
interface PromptInputProps { |
|
|
onPromptChange: (prompt: string) => void; |
|
|
defaultPrompt?: string; |
|
|
} |
|
|
|
|
|
export default function PromptInput({ |
|
|
onPromptChange, |
|
|
defaultPrompt = PROMPTS.default, |
|
|
}: PromptInputProps) { |
|
|
const [prompt, setPrompt] = useState(defaultPrompt); |
|
|
const [showSuggestions, setShowSuggestions] = useState(false); |
|
|
const inputRef = useRef<HTMLTextAreaElement>(null); |
|
|
const containerRef = useRef<HTMLDivElement>(null); |
|
|
|
|
|
const resizeTextarea = () => { |
|
|
if (inputRef.current) { |
|
|
inputRef.current.style.height = "auto"; |
|
|
const newHeight = Math.min(inputRef.current.scrollHeight, 200); |
|
|
inputRef.current.style.height = `${newHeight}px`; |
|
|
} |
|
|
}; |
|
|
|
|
|
useEffect(() => { |
|
|
onPromptChange(prompt); |
|
|
resizeTextarea(); |
|
|
}, [prompt, onPromptChange]); |
|
|
|
|
|
const handleInputFocus = () => setShowSuggestions(true); |
|
|
const handleInputClick = () => setShowSuggestions(true); |
|
|
|
|
|
const handleInputBlur = (e: React.FocusEvent) => { |
|
|
|
|
|
requestAnimationFrame(() => { |
|
|
if ( |
|
|
!e.relatedTarget || |
|
|
!containerRef.current?.contains(e.relatedTarget as Node) |
|
|
) { |
|
|
setShowSuggestions(false); |
|
|
} |
|
|
}); |
|
|
}; |
|
|
|
|
|
const handleSuggestionClick = (suggestion: string) => { |
|
|
setPrompt(suggestion); |
|
|
setShowSuggestions(false); |
|
|
inputRef.current?.focus(); |
|
|
}; |
|
|
|
|
|
const clearInput = () => { |
|
|
setPrompt(""); |
|
|
inputRef.current?.focus(); |
|
|
}; |
|
|
|
|
|
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { |
|
|
setPrompt(e.target.value); |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div |
|
|
ref={containerRef} |
|
|
className="w-full max-w-xl relative group font-sans" |
|
|
> |
|
|
{/* Suggestions Panel */} |
|
|
<div |
|
|
className={`absolute bottom-full left-0 right-0 mb-3 transition-all duration-300 ease-out transform origin-bottom ${ |
|
|
showSuggestions |
|
|
? "opacity-100 translate-y-0 scale-100 pointer-events-auto" |
|
|
: "opacity-0 translate-y-2 scale-95 pointer-events-none" |
|
|
}`} |
|
|
> |
|
|
<div |
|
|
className="bg-white rounded-lg shadow-xl border overflow-hidden" |
|
|
style={{ borderColor: THEME.beigeDark }} |
|
|
> |
|
|
{/* Header */} |
|
|
<div |
|
|
className="border-b px-4 py-2 flex items-center space-x-2" |
|
|
style={{ |
|
|
backgroundColor: THEME.beigeLight, |
|
|
borderColor: THEME.beigeDark, |
|
|
}} |
|
|
> |
|
|
<svg |
|
|
className="w-3 h-3" |
|
|
style={{ color: THEME.mistralOrange }} |
|
|
fill="none" |
|
|
viewBox="0 0 24 24" |
|
|
stroke="currentColor" |
|
|
> |
|
|
<path |
|
|
strokeLinecap="round" |
|
|
strokeLinejoin="round" |
|
|
strokeWidth={2} |
|
|
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" |
|
|
/> |
|
|
</svg> |
|
|
<span className="text-xs font-bold uppercase tracking-wider text-gray-500"> |
|
|
Prompt Library |
|
|
</span> |
|
|
</div> |
|
|
{/* List */} |
|
|
<ul className="py-2"> |
|
|
{PROMPTS.suggestions.map((suggestion, index) => ( |
|
|
<li |
|
|
key={index} |
|
|
tabIndex={0} |
|
|
onMouseDown={(e) => e.preventDefault()} // Prevent blur |
|
|
onClick={() => handleSuggestionClick(suggestion)} |
|
|
className="px-4 py-2.5 cursor-pointer flex items-start gap-3 transition-colors hover:bg-[var(--mistral-beige-light)] group/item" |
|
|
> |
|
|
<span |
|
|
className="mt-1 opacity-0 group-hover/item:opacity-100 transition-opacity text-xs font-mono" |
|
|
style={{ color: THEME.mistralOrange }} |
|
|
> |
|
|
{`>`} |
|
|
</span> |
|
|
<span className="text-sm text-gray-700 group-hover/item:text-black leading-snug"> |
|
|
{suggestion} |
|
|
</span> |
|
|
</li> |
|
|
))} |
|
|
</ul> |
|
|
</div> |
|
|
</div> |
|
|
{/* Input Container */} |
|
|
<div className="relative"> |
|
|
{/* Label Badge */} |
|
|
<div className="absolute -top-3 left-4 z-10"> |
|
|
<span |
|
|
className="border text-[10px] font-bold text-gray-500 uppercase tracking-widest px-2 py-0.5 rounded-sm" |
|
|
style={{ |
|
|
backgroundColor: THEME.beigeLight, |
|
|
borderColor: THEME.beigeDark, |
|
|
}} |
|
|
> |
|
|
Prompt |
|
|
</span> |
|
|
</div> |
|
|
<div |
|
|
className={` |
|
|
relative bg-white rounded-lg shadow-lg border transition-all duration-300 |
|
|
${showSuggestions ? "border-[var(--mistral-orange)] ring-1 ring-[var(--mistral-orange)]/20" : "border-[var(--mistral-beige-dark)] hover:border-[#D0C5A0]"} |
|
|
`} |
|
|
> |
|
|
<div className="flex items-start p-1"> |
|
|
<textarea |
|
|
ref={inputRef} |
|
|
value={prompt} |
|
|
onChange={handleInputChange} |
|
|
onFocus={handleInputFocus} |
|
|
onBlur={handleInputBlur} |
|
|
onClick={handleInputClick} |
|
|
className="w-full py-4 pl-5 pr-10 bg-transparent text-lg md:text-xl font-mono resize-none focus:outline-none placeholder:text-gray-300 leading-relaxed" |
|
|
style={{ |
|
|
minHeight: "60px", |
|
|
maxHeight: "200px", |
|
|
overflowY: "auto", |
|
|
color: THEME.textBlack, |
|
|
}} |
|
|
placeholder={PROMPTS.placeholder} |
|
|
rows={1} |
|
|
/> |
|
|
{/* Clear Button */} |
|
|
{prompt && ( |
|
|
<button |
|
|
type="button" |
|
|
onMouseDown={(e) => e.preventDefault()} |
|
|
onClick={clearInput} |
|
|
className="absolute right-3 top-5 text-gray-300 hover:text-[var(--mistral-orange)] transition-colors p-1" |
|
|
aria-label="Clear prompt" |
|
|
> |
|
|
<svg |
|
|
xmlns="http://www.w3.org/2000/svg" |
|
|
className="h-6 w-6" |
|
|
viewBox="0 0 20 20" |
|
|
fill="currentColor" |
|
|
> |
|
|
<path |
|
|
fillRule="evenodd" |
|
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" |
|
|
clipRule="evenodd" |
|
|
/> |
|
|
</svg> |
|
|
</button> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|