chat-input.tsx
styles.css
prompt.md
Collapse
Reset
Copy Code
Download
Run
1
"use client";
2
3
import React, { useState, useEffect, useRef } from "react";
4
import { motion, AnimatePresence } from "framer-motion";
5
import { Plus, Mic, ChevronDown, Check, X, Sun, Sparkles, Star, CircleDashed, Hexagon, Triangle } from "lucide-react";
6
import { cn } from "@/lib/utils";
7
8
const MODELS = [
9
{ id: 'opus', name: 'Claude Opus 5.5', shortName: 'Opus 5.5', icon: <Sun className="w-3.5 h-3.5 text-orange-500" /> },
10
{ id: 'gpt6', name: 'GPT-6 Astra', shortName: 'GPT-6', icon: <Sparkles className="w-3.5 h-3.5 text-emerald-500" /> },
11
{ id: 'gemini', name: 'Gemini 3.1 Pro', shortName: 'Gemini 3.1', icon: <Star className="w-3.5 h-3.5 text-blue-400" /> },
12
{ id: 'fable', name: 'Claude Fable 5.1', shortName: 'Fable 5.1', icon: <Sun className="w-3.5 h-3.5 text-orange-400" /> },
13
{ id: 'grok', name: 'Grok 4.7', shortName: 'Grok 4.7', icon: <CircleDashed className="w-3.5 h-3.5 text-zinc-400" /> },
14
{ id: 'deepseek', name: 'DeepSeek-V4-Pro', shortName: 'DeepSeek', icon: <Hexagon className="w-3.5 h-3.5 text-blue-500" /> },
15
{ id: 'qwen', name: 'Qwen3.8-Max', shortName: 'Qwen3.8', icon: <Triangle className="w-3.5 h-3.5 text-purple-500" /> },
16
{ id: 'kimi', name: 'Kimi K3', shortName: 'Kimi K3', icon: <div className="text-[12px] font-bold text-zinc-400 font-mono leading-none">K</div> },
17
];
18
19
const NUM_BARS = 60;
20
21
export function ChatInput({ className }: { className?: string }) {
22
const [state, setState] = useState<'idle' | 'mic-voice' | 'mic-progress' | 'blue-voice' | 'blue-progress'>('idle');
23
const [selectedModel, setSelectedModel] = useState(MODELS[0]);
24
const [isSelectorOpen, setIsSelectorOpen] = useState(false);
25
const [progress, setProgress] = useState(0);
26
const [voiceHeights, setVoiceHeights] = useState<number[]>(Array(NUM_BARS).fill(4));
27
const selectorRef = useRef<HTMLDivElement>(null);
28
29
const TRANSCRIPTION = "Damn, I think Sunflower might just be the greatest component library ever";
30
const words = TRANSCRIPTION.split(" ");
31
32
// Click outside to close selector
33
useEffect(() => {
34
const handleClickOutside = (e: MouseEvent) => {
35
if (selectorRef.current && !selectorRef.current.contains(e.target as Node)) {
36
setIsSelectorOpen(false);
37
}
38
};
39
if (isSelectorOpen) document.addEventListener('mousedown', handleClickOutside);
40
return () => document.removeEventListener('mousedown', handleClickOutside);
41
}, [isSelectorOpen]);
42
43
// Voice Waveform Animation Loop (for mic-voice)
44
useEffect(() => {
45
if (state !== 'mic-voice') return;
46
let frame: number;
47
let lastUpdate = 0;
48
const animate = (time: number) => {
49
if (time - lastUpdate > 100) {
50
setVoiceHeights(prev => prev.map((_, i) => {
51
const envelope = Math.sin((i / (NUM_BARS - 1)) * Math.PI);
52
return 4 + Math.random() * 24 * envelope;
53
}));
54
lastUpdate = time;
55
}
56
frame = requestAnimationFrame(animate);
57
};
58
frame = requestAnimationFrame(animate);
59
return () => cancelAnimationFrame(frame);
60
}, [state]);
61
62
// Progress Animation Loop (for mic-progress and blue-progress)
63
useEffect(() => {
64
if (state !== 'mic-progress' && state !== 'blue-progress') {
65
setProgress(0);
66
return;
67
}
68
let frame: number;
69
const start = performance.now();
70
const DURATION = 2000; // 2 seconds processing
71
const animate = (time: number) => {
72
const elapsed = time - start;
73
const p = Math.min(elapsed / DURATION, 1);
74
setProgress(p);
75
if (p < 1) {
76
frame = requestAnimationFrame(animate);
77
} else {
78
setTimeout(() => setState('idle'), 400);
79
}
80
};
81
frame = requestAnimationFrame(animate);
82
return () => cancelAnimationFrame(frame);
83
}, [state]);
84
85
return (
86
<div className={cn("relative w-full max-w-[600px] flex justify-center", className)}>
87
88
{/* Floating Transcription Text (Blue Voice only) */}
89
<AnimatePresence>
90
{state === 'blue-voice' && (
91
<motion.div
92
initial="hidden"
93
animate="visible"
94
exit="hidden"
95
variants={{
96
visible: { transition: { staggerChildren: 0.15 } },
97
hidden: {}
98
}}
99
className="absolute bottom-full mb-6 left-0 w-full flex justify-center text-center z-20 pointer-events-none"
100
>
101
<div className="text-zinc-800 dark:text-zinc-200 font-medium text-[15px] max-w-[280px] leading-relaxed">
102
{words.map((word, i) => (
103
<motion.span
104
key={i}
105
variants={{
106
hidden: { opacity: 0, filter: 'blur(4px)', y: 4 },
107
visible: { opacity: 1, filter: 'blur(0px)', y: 0 }
108
}}
109
transition={{ duration: 0.4 }}
110
className="inline-block mr-[4px]"
111
>
112
{word}
113
</motion.span>
114
))}
115
</div>
116
</motion.div>
117
)}
118
</AnimatePresence>
119
120
<div className="flex items-center w-full h-[52px] bg-white dark:bg-[#222222] rounded-full px-2 border border-black/5 dark:border-white/5 shadow-md relative z-10">
121
122
{/* Glowing Aura Background (Blue states only) */}
123
<AnimatePresence>
124
{(state === 'blue-voice' || state === 'blue-progress') && (
125
<motion.div
126
initial={{ opacity: 0 }}
127
animate={{ opacity: state === 'blue-voice' ? 1 : 0.5 }}
128
exit={{ opacity: 0 }}
129
transition={{ duration: 0.5 }}
130
className="absolute inset-0 overflow-hidden rounded-full pointer-events-none z-0 bg-purple-50/50 dark:bg-purple-900/10"
131
>
132
<motion.div
133
animate={{ backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"] }}
134
transition={{ duration: 6, repeat: Infinity, ease: "linear" }}
135
className="absolute -bottom-[4px] left-0 w-[200%] h-[20px] bg-gradient-to-r from-emerald-400/30 via-purple-500/40 to-orange-400/30 blur-[10px]"
136
style={{ backgroundSize: "200% 100%" }}
137
/>
138
<motion.div
139
animate={{ backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"] }}
140
transition={{ duration: 6, repeat: Infinity, ease: "linear" }}
141
className="absolute -bottom-[2px] left-0 w-[200%] h-[4px] bg-gradient-to-r from-emerald-400/60 via-purple-500/70 to-orange-400/60 dark:from-emerald-300/80 dark:via-purple-400/90 dark:to-orange-300/80 blur-[2px]"
142
style={{ backgroundSize: "200% 100%" }}
143
/>
144
</motion.div>
145
)}
146
</AnimatePresence>
147
148
<div className="flex items-center justify-center w-10 shrink-0 h-full relative z-10">
149
<button className="text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors">
150
<Plus className="w-5 h-5" />
151
</button>
152
</div>
153
154
<div className="flex-1 relative h-full flex items-center justify-between z-10">
155
<AnimatePresence mode="wait">
156
157
{/* IDLE & BLUE FLOW */}
158
{(state === 'idle' || state.startsWith('blue')) && (
159
<motion.div
160
key="input-view"
161
initial={{ opacity: 0, y: 10 }}
162
animate={{ opacity: 1, y: 0 }}
163
exit={{ opacity: 0, y: -10 }}
164
transition={{ duration: 0.2 }}
165
className="absolute inset-0 flex items-center justify-between"
166
>
167
<input
168
placeholder="Ask anything"
169
className="flex-1 h-full bg-transparent outline-none text-[15px] text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 px-1"
170
/>
171
172
<div className="flex items-center gap-2.5 shrink-0 pr-1">
173
<div className="relative" ref={selectorRef}>
174
<button
175
onClick={() => setIsSelectorOpen(!isSelectorOpen)}
176
className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-transparent hover:bg-black/5 dark:hover:bg-white/5 transition-colors text-[13px] font-medium text-zinc-700 dark:text-zinc-300"
177
>
178
{selectedModel.shortName}
179
<ChevronDown className="w-3.5 h-3.5 text-zinc-500" />
180
</button>
181
182
<AnimatePresence>
183
{isSelectorOpen && (
184
<motion.div
185
initial={{ opacity: 0, scale: 0.95, y: 5 }}
186
animate={{ opacity: 1, scale: 1, y: 0 }}
187
exit={{ opacity: 0, scale: 0.95, y: 5 }}
188
transition={{ duration: 0.15 }}
189
className="absolute top-full right-0 mt-3 w-[220px] bg-white dark:bg-[#1C1C1C] border border-black/5 dark:border-white/10 rounded-xl p-1.5 shadow-2xl flex flex-col z-50 overflow-hidden"
190
>
191
{MODELS.map(m => (
192
<button
193
key={m.id}
194
onClick={() => {
195
setSelectedModel(m);
196
setIsSelectorOpen(false);
197
}}
198
className="flex items-center justify-between w-full px-3 py-2.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg text-[13px] text-zinc-700 dark:text-zinc-200 transition-colors"
199
>
200
<div className="flex items-center gap-3">
201
{m.icon}
202
<span>{m.name}</span>
203
</div>
204
{selectedModel.id === m.id && <Check className="w-3.5 h-3.5 text-zinc-900 dark:text-zinc-400" />}
205
</button>
206
))}
207
</motion.div>
208
)}
209
</AnimatePresence>
210
</div>
211
212
<button
213
onClick={() => setState('mic-voice')}
214
className="p-1.5 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 transition-colors rounded-full"
215
>
216
<Mic className="w-4 h-4" />
217
</button>
218
219
<button
220
onClick={() => {
221
if (state === 'idle') setState('blue-voice');
222
else if (state === 'blue-voice') setState('blue-progress');
223
}}
224
className="relative flex items-center justify-center w-[34px] h-[34px] rounded-full bg-[#1A85FF] hover:bg-blue-500 transition-colors shadow-sm"
225
>
226
<AnimatePresence mode="wait">
227
{state === 'idle' && (
228
<motion.div key="wave" initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }} transition={{ duration: 0.15 }}>
229
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round">
230
<path d="M12 4v16M8 9v6M16 9v6M4 11v2M20 11v2" />
231
</svg>
232
</motion.div>
233
)}
234
{state === 'blue-voice' && (
235
<motion.div key="stop" initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }} transition={{ duration: 0.15 }}>
236
<div className="w-2.5 h-2.5 bg-white rounded-[2px]" />
237
</motion.div>
238
)}
239
{state === 'blue-progress' && (
240
<motion.div key="spinner" initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }} transition={{ duration: 0.15 }}>
241
<div className="w-[14px] h-[14px] border-[2px] border-white/30 border-t-white rounded-full animate-spin" />
242
</motion.div>
243
)}
244
</AnimatePresence>
245
</button>
246
</div>
247
</motion.div>
248
)}
249
250
{/* MIC FLOW (60 Bars) */}
251
{(state.startsWith('mic')) && (
252
<motion.div
253
key="mic-view"
254
initial={{ opacity: 0, scale: 0.95 }}
255
animate={{ opacity: 1, scale: 1 }}
256
exit={{ opacity: 0, scale: 0.95 }}
257
transition={{ duration: 0.2 }}
258
className="absolute inset-0 flex items-center justify-between"
259
>
260
<div className="flex-1 flex items-center justify-center gap-[2px] px-4 h-full">
261
{Array.from({ length: NUM_BARS }).map((_, i) => {
262
const isProgress = state === 'mic-progress';
263
const isFilled = isProgress && (i / NUM_BARS) < progress;
264
const height = state === 'mic-voice' ? voiceHeights[i] : 4;
265
266
return (
267
<motion.div
268
key={i}
269
animate={{ height }}
270
transition={{ duration: state === 'mic-voice' ? 0.15 : 0.3 }}
271
className={cn(
272
"w-[2px] rounded-full",
273
isProgress
274
? (isFilled ? "bg-zinc-800 dark:bg-zinc-200" : "bg-zinc-200 dark:bg-zinc-700")
275
: "bg-zinc-800 dark:bg-zinc-300"
276
)}
277
/>
278
);
279
})}
280
</div>
281
282
<div className="flex items-center gap-2 shrink-0 pr-2">
283
{state === 'mic-voice' ? (
284
<button
285
onClick={() => setState('mic-progress')}
286
className="flex items-center justify-center w-7 h-7 text-zinc-400 dark:text-zinc-500 hover:text-emerald-500 dark:hover:text-emerald-400 hover:bg-black/5 dark:hover:bg-white/10 transition-colors rounded-full"
287
>
288
<Check className="w-4 h-4" />
289
</button>
290
) : (
291
<div className="flex items-center justify-center w-7 h-7">
292
<div className="w-[16px] h-[16px] rounded-full border-[2px] border-zinc-200 dark:border-zinc-600 border-t-zinc-800 dark:border-t-zinc-300 animate-spin" />
293
</div>
294
)}
295
<button
296
onClick={() => setState('idle')}
297
className="flex items-center justify-center w-7 h-7 text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300 hover:bg-black/5 dark:hover:bg-white/10 transition-colors rounded-full"
298
>
299
<X className="w-4 h-4" />
300
</button>
301
</div>
302
</motion.div>
303
)}
304
305
</AnimatePresence>
306
</div>
307
</div>
308
</div>
309
);
310
}
311

