| | 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> |
| | ); |
| | } |
| |
|