Components
Loading preview...
AI Prompt Input Box
npx shadcn@latest add https://21st.dev/r/arunjdass/ai-prompt-input-boximport React, { useState, useRef, useEffect } from 'react';
import {
Paperclip,
Mic,
ArrowUp,
Globe,
BrainCircuit,
ChevronDown,
Square,
Check,
Folder
} from 'lucide-react';
const MODELS = [
{ id: 'opus', name: 'Opus 4.8', icon: 'bg-purple-400', shadow: 'shadow-[0_0_8px_rgba(192,132,252,0.5)]' },
{ id: 'gpt', name: 'GPT 5.5', icon: 'bg-emerald-400', shadow: 'shadow-[0_0_8px_rgba(52,211,153,0.5)]' },
{ id: 'gemini', name: 'Gemini 3.1 Pro', icon: 'bg-blue-400', shadow: 'shadow-[0_0_8px_rgba(96,165,250,0.5)]' }
];
export const AiChatInput = () => {
const [value, setValue] = useState('');
const [isFocused, setIsFocused] = useState(false);
const [activeModel, setActiveModel] = useState(MODELS[0]);
const [isModelSelectorOpen, setIsModelSelectorOpen] = useState(false);
const [isWebSearchActive, setIsWebSearchActive] = useState(false);
const [isReasoningActive, setIsReasoningActive] = useState(true);
const [isRecording, setIsRecording] = useState(false);
const [recordingTime, setRecordingTime] = useState(0);
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
let interval: ReturnType<typeof setInterval>;
if (isRecording) {
interval = setInterval(() => {
setRecordingTime((prev) => prev + 1);
}, 1000);
} else {
setRecordingTime(0);
}
return () => clearInterval(interval);
}, [isRecording]);
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'inherit';
const scrollHeight = textareaRef.current.scrollHeight;
textareaRef.current.style.height = `${Math.min(scrollHeight, 200)}px`;
}
}, [value, isRecording]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (value.trim()) {
setValue('');
}
}
};
const toggleRecord = () => {
setIsRecording(!isRecording);
if (!isRecording) {
setValue('');
}
};
return (
<div className="relative w-full h-full min-h-screen flex items-center justify-center p-4 sm:p-8 font-sans overflow-hidden bg-black/95">
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-[20%] left-[30%] w-[500px] h-[500px] bg-indigo-500/10 rounded-full blur-[120px] mix-blend-screen" />
<div className="absolute bottom-[20%] right-[20%] w-[600px] h-[600px] bg-purple-500/10 rounded-full blur-[130px] mix-blend-screen" />
<div className="absolute top-[50%] left-[50%] -translate-x-1/2 -translate-y-1/2 w-[800px] h-[300px] bg-blue-500/10 rounded-full blur-[150px] mix-blend-screen" />
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-[0.03] mix-blend-overlay" />
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.02)_1px,transparent_1px)] bg-[size:32px_32px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_20%,transparent_100%)]" />
</div>
<style>{`
@keyframes audio-wave {
0%, 100% { height: 6px; }
50% { height: 24px; }
}
`}</style>
<div className="w-full max-w-2xl relative z-10 flex flex-col gap-4">
<div
className={`relative flex flex-col w-full rounded-[24px] transition-all duration-300 ease-out
${isFocused || isRecording || isModelSelectorOpen
? 'bg-zinc-900/80 backdrop-blur-2xl border border-indigo-500/40 shadow-[0_0_30px_rgba(99,102,241,0.15)] ring-4 ring-indigo-500/10'
: 'bg-zinc-900/60 backdrop-blur-xl border border-white/10 shadow-2xl hover:bg-zinc-900/70 hover:border-white/15'
}
`}
>
<div className="pl-5 pr-3 pt-4 pb-0 relative">
{isRecording ? (
<div className="flex items-center gap-3 h-[44px] px-2 animate-in fade-in duration-300">
<span className="w-2 h-2 rounded-full bg-rose-500 animate-pulse shadow-[0_0_10px_rgba(244,63,94,0.5)] mt-[2px]" />
<span className="text-sm font-medium text-rose-400/90 mt-[1px]">Listening...</span>
<span className="text-sm font-medium text-rose-400/60 mt-[1px] ml-1">{formatTime(recordingTime)}</span>
<div className="flex items-center gap-[3px] ml-2 h-[24px]">
<div className="w-1 bg-rose-400/60 rounded-full" style={{ animation: 'audio-wave 1.2s ease-in-out infinite 0.0s' }} />
<div className="w-1 bg-rose-400/80 rounded-full" style={{ animation: 'audio-wave 1.2s ease-in-out infinite 0.2s' }} />
<div className="w-1 bg-rose-400/60 rounded-full" style={{ animation: 'audio-wave 1.2s ease-in-out infinite 0.4s' }} />
<div className="w-1 bg-rose-400/80 rounded-full" style={{ animation: 'audio-wave 1.2s ease-in-out infinite 0.1s' }} />
<div className="w-1 bg-rose-400/60 rounded-full" style={{ animation: 'audio-wave 1.2s ease-in-out infinite 0.3s' }} />
</div>
</div>
) : (
<textarea
ref={textareaRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onFocus={() => {
setIsFocused(true);
setIsModelSelectorOpen(false);
}}
onBlur={() => setIsFocused(false)}
onKeyDown={handleKeyDown}
placeholder="Start typing or record to ask for anything"
className="w-full resize-none bg-transparent outline-none text-white/90 text-[14px] leading-[20px] placeholder:text-white/30 custom-scrollbar"
style={{
minHeight: '24px',
maxHeight: '200px'
}}
/>
)}
</div>
<div className="flex items-center justify-between px-2 pb-2 pt-0">
<div className="flex items-center gap-0.5">
<button
className="p-1.5 rounded-lg text-white/40 hover:text-white/90 hover:bg-white/10 transition-all duration-200 hover:scale-105 active:scale-95"
title="Attach files"
>
<Paperclip className="w-4 h-4" />
</button>
<button
className="p-1.5 rounded-lg text-white/40 hover:text-white/90 hover:bg-white/10 transition-all duration-200 hover:scale-105 active:scale-95"
title="Browse folders & media"
>
<Folder className="w-4 h-4" />
</button>
<button
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium transition-all duration-200 hover:scale-105 active:scale-95 ml-1
${isWebSearchActive
? 'bg-blue-500/15 text-blue-400 hover:bg-blue-500/25 border border-blue-500/20'
: 'text-white/40 hover:text-white/90 hover:bg-white/10 border border-transparent'
}
`}
onClick={() => setIsWebSearchActive(!isWebSearchActive)}
>
<Globe className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Search</span>
</button>
<button
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium transition-all duration-200 hover:scale-105 active:scale-95
${isReasoningActive
? 'bg-purple-500/15 text-purple-400 hover:bg-purple-500/25 border border-purple-500/20'
: 'text-white/40 hover:text-white/90 hover:bg-white/10 border border-transparent'
}
`}
onClick={() => setIsReasoningActive(!isReasoningActive)}
>
<BrainCircuit className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Reason</span>
</button>
</div>
<div className="flex items-center gap-1.5">
<div className="relative">
<button
onClick={() => setIsModelSelectorOpen(!isModelSelectorOpen)}
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-white/40 hover:text-white/90 hover:bg-white/10 transition-all duration-200 hover:scale-105 active:scale-95 text-[11px] font-medium"
>
<span className={`w-1.5 h-1.5 rounded-full ${activeModel.icon} ${activeModel.shadow}`} />
<span className="hidden sm:inline">{activeModel.name}</span>
<ChevronDown className="w-3 h-3 ml-0.5 opacity-50" />
</button>
{isModelSelectorOpen && (
<div className="absolute bottom-full right-0 mb-2 w-48 bg-zinc-900/95 backdrop-blur-xl border border-white/10 rounded-xl shadow-2xl overflow-hidden z-50 animate-in fade-in slide-in-from-bottom-2 duration-200">
<div className="p-1.5 flex flex-col gap-0.5">
{MODELS.map((m) => (
<button
key={m.id}
onClick={() => {
setActiveModel(m);
setIsModelSelectorOpen(false);
}}
className="flex items-center justify-between w-full px-2.5 py-2 rounded-lg text-left text-xs font-medium hover:bg-white/10 transition-colors group"
>
<div className="flex items-center gap-2">
<span className={`w-1.5 h-1.5 rounded-full ${m.icon} shadow-[0_0_8px_rgba(255,255,255,0.2)]`} />
<span className={activeModel.id === m.id ? 'text-white' : 'text-white/70 group-hover:text-white/90'}>
{m.name}
</span>
</div>
{activeModel.id === m.id && <Check className="w-3.5 h-3.5 text-white/90" />}
</button>
))}
</div>
</div>
)}
</div>
{value.length === 0 && (
<button
onClick={toggleRecord}
className={`flex items-center justify-center w-9 h-9 rounded-xl transition-all duration-300 hover:scale-105 active:scale-95
${isRecording
? 'text-rose-400 bg-rose-500/15 hover:bg-rose-500/25 shadow-[0_0_15px_rgba(244,63,94,0.15)] ring-1 ring-rose-500/30'
: 'text-white/40 hover:text-white/90 hover:bg-white/10'
}
`}
title={isRecording ? "Stop recording" : "Voice input"}
>
{isRecording ? <Square className="w-5 h-5 fill-rose-400/20" /> : <Mic className="w-5 h-5" />}
</button>
)}
{!isRecording && (
<button
disabled={value.trim().length === 0}
className={`flex items-center justify-center w-9 h-9 rounded-xl transition-all duration-300 active:scale-95
${value.trim().length > 0
? 'bg-white text-black hover:bg-white/90 hover:scale-105 hover:shadow-[0_0_20px_rgba(255,255,255,0.3)] shadow-md cursor-pointer'
: 'bg-white/5 text-white/20 cursor-not-allowed'
}
`}
>
<ArrowUp className="w-5 h-5" strokeWidth={2.5} />
</button>
)}
</div>
</div>
</div>
</div>
</div>
);
};
export default AiChatInput;