style: modernize chat widget with glassmorphism and improve mobile layout
This commit is contained in:
@@ -221,15 +221,15 @@ export default function ChatWidget() {
|
|||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="fixed bottom-20 left-4 md:bottom-6 md:left-6 z-30 bg-stone-800/90 backdrop-blur-md text-stone-50 p-3 rounded-full shadow-xl hover:bg-stone-700/90 hover:scale-110 transition-all duration-300 group cursor-pointer border border-white/10"
|
className="fixed bottom-4 left-4 md:bottom-6 md:left-6 z-30 bg-stone-800/80 backdrop-blur-md text-stone-50 p-3.5 rounded-full shadow-[0_8px_20px_rgba(41,37,36,0.2)] hover:bg-stone-800 hover:scale-105 transition-all duration-300 group cursor-pointer border border-white/10 ring-1 ring-white/20"
|
||||||
aria-label="Open chat"
|
aria-label="Open chat"
|
||||||
>
|
>
|
||||||
<MessageCircle size={20} />
|
<MessageCircle size={24} />
|
||||||
<span className="absolute -top-1 -right-1 w-3 h-3 bg-stone-400 rounded-full animate-pulse shadow-sm" />
|
<span className="absolute top-0 right-0 w-3 h-3 bg-stone-400 rounded-full animate-pulse shadow-sm border-2 border-stone-800" />
|
||||||
|
|
||||||
{/* Tooltip */}
|
{/* Tooltip */}
|
||||||
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-1.5 bg-stone-900/90 text-stone-50 text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-[100] shadow-lg">
|
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 px-3 py-1.5 bg-stone-900/90 text-stone-50 text-xs font-medium rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-[100] shadow-xl backdrop-blur-sm">
|
||||||
Chat with AI assistant
|
Chat with AI
|
||||||
</span>
|
</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@@ -240,42 +240,42 @@ export default function ChatWidget() {
|
|||||||
{isOpen && (
|
{isOpen && (
|
||||||
<motion.div
|
<motion.div
|
||||||
data-chat-widget
|
data-chat-widget
|
||||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
initial={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(10px)" }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }}
|
||||||
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
exit={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(10px)" }}
|
||||||
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
transition={{ type: "spring", damping: 30, stiffness: 400 }}
|
||||||
className="fixed bottom-20 left-4 md:bottom-6 md:left-6 z-30 w-[300px] sm:w-[340px] md:w-[380px] max-w-[calc(100vw-2rem)] h-[450px] sm:h-[500px] md:h-[550px] max-h-[calc(100vh-10rem)] bg-[#fdfcf8]/90 backdrop-blur-xl rounded-2xl shadow-2xl flex flex-col overflow-hidden border border-stone-200/50 [&_a]:text-inherit [&_a]:no-underline [&_a]:text-stone-800 [&_a:hover]:text-stone-900 [&_a:visited]:text-stone-800 [&_a:active]:text-stone-900 [&_*]:outline-none [&_*:focus]:outline-none [&_*:focus-visible]:outline-none"
|
className="fixed bottom-20 left-4 right-4 md:bottom-24 md:left-6 md:right-auto z-30 md:w-[380px] h-[60vh] md:h-[550px] max-h-[600px] bg-[#fdfcf8]/60 backdrop-blur-2xl saturate-150 rounded-2xl shadow-[0_12px_40px_rgba(41,37,36,0.15)] flex flex-col overflow-hidden border border-white/40 ring-1 ring-white/50"
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-[#f3f1e7]/80 backdrop-blur-md text-stone-800 p-3 md:p-4 flex items-center justify-between border-b border-stone-200/50">
|
<div className="bg-white/40 backdrop-blur-lg text-stone-800 p-4 flex items-center justify-between border-b border-white/30">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="w-10 h-10 rounded-full bg-white/50 backdrop-blur-sm flex items-center justify-center ring-1 ring-stone-200 shadow-sm">
|
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-stone-100 to-white flex items-center justify-center ring-1 ring-white shadow-sm">
|
||||||
<Sparkles size={20} className="text-stone-600" />
|
<Sparkles size={18} className="text-stone-600" />
|
||||||
</div>
|
</div>
|
||||||
<span className="absolute bottom-0 right-0 w-3 h-3 bg-stone-400 rounded-full border-2 border-[#f3f1e7] shadow-sm" />
|
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-emerald-400 rounded-full border-2 border-white shadow-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<h3 className="font-bold text-sm truncate text-stone-800">
|
<h3 className="font-bold text-sm truncate text-stone-900 tracking-tight">
|
||||||
Dennis{"'"}s AI Assistant
|
Assistant
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-stone-500 truncate">
|
<p className="text-[11px] font-medium text-stone-500 truncate">
|
||||||
Always online
|
Powered by AI
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={clearChat}
|
onClick={clearChat}
|
||||||
className="p-2 hover:bg-stone-200/50 rounded-lg transition-colors text-stone-500 hover:text-stone-800"
|
className="p-2 hover:bg-stone-200/40 rounded-full transition-colors text-stone-500 hover:text-red-500"
|
||||||
title="Clear conversation"
|
title="Clear conversation"
|
||||||
>
|
>
|
||||||
<Trash2 size={18} />
|
<Trash2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
className="p-2 hover:bg-stone-200/50 rounded-lg transition-colors text-stone-500 hover:text-stone-800"
|
className="p-2 hover:bg-stone-200/40 rounded-full transition-colors text-stone-500 hover:text-stone-900"
|
||||||
aria-label="Close chat"
|
aria-label="Close chat"
|
||||||
>
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
@@ -284,7 +284,7 @@ export default function ChatWidget() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div className="flex-1 overflow-y-auto scrollbar-hide p-3 md:p-4 space-y-3 md:space-y-4 bg-transparent">
|
<div className="flex-1 overflow-y-auto scrollbar-hide p-4 space-y-4 bg-transparent">
|
||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={message.id}
|
key={message.id}
|
||||||
@@ -293,21 +293,21 @@ export default function ChatWidget() {
|
|||||||
className={`flex ${message.sender === "user" ? "justify-end" : "justify-start"}`}
|
className={`flex ${message.sender === "user" ? "justify-end" : "justify-start"}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`max-w-[80%] rounded-2xl px-4 py-2.5 backdrop-blur-sm ${
|
className={`max-w-[85%] rounded-2xl px-4 py-3 backdrop-blur-md shadow-sm ${
|
||||||
message.sender === "user"
|
message.sender === "user"
|
||||||
? "bg-stone-800 text-stone-50 shadow-md"
|
? "bg-stone-800/90 text-stone-50"
|
||||||
: "bg-white/80 backdrop-blur-sm text-stone-800 border border-stone-100 shadow-sm"
|
: "bg-white/60 text-stone-800 border border-white/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<p className={`text-sm whitespace-pre-wrap break-words leading-relaxed [&_a]:text-inherit [&_a]:no-underline [&_a]:text-current [&_a:hover]:text-current [&_a:visited]:text-current [&_a:active]:text-current ${
|
<p className={`text-sm whitespace-pre-wrap break-words leading-relaxed ${
|
||||||
message.sender === "user" ? "text-stone-50" : "text-stone-800"
|
message.sender === "user" ? "text-stone-50 font-light" : "text-stone-800 font-normal"
|
||||||
}`}>
|
}`}>
|
||||||
{message.text}
|
{message.text}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className={`text-[10px] mt-1.5 ${
|
className={`text-[10px] mt-1.5 ${
|
||||||
message.sender === "user"
|
message.sender === "user"
|
||||||
? "text-stone-300"
|
? "text-stone-400"
|
||||||
: "text-stone-400"
|
: "text-stone-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -327,11 +327,11 @@ export default function ChatWidget() {
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
className="flex justify-start"
|
className="flex justify-start"
|
||||||
>
|
>
|
||||||
<div className="bg-white/80 backdrop-blur-sm border border-stone-100 rounded-2xl px-4 py-3 shadow-sm">
|
<div className="bg-white/50 backdrop-blur-sm border border-white/40 rounded-2xl px-4 py-3 shadow-sm">
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-2 h-2 bg-stone-400 rounded-full"
|
className="w-1.5 h-1.5 bg-stone-400 rounded-full"
|
||||||
animate={{ y: [0, -8, 0] }}
|
animate={{ y: [0, -6, 0] }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 0.6,
|
duration: 0.6,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
@@ -339,8 +339,8 @@ export default function ChatWidget() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-2 h-2 bg-stone-400 rounded-full"
|
className="w-1.5 h-1.5 bg-stone-400 rounded-full"
|
||||||
animate={{ y: [0, -8, 0] }}
|
animate={{ y: [0, -6, 0] }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 0.6,
|
duration: 0.6,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
@@ -348,8 +348,8 @@ export default function ChatWidget() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-2 h-2 bg-stone-400 rounded-full"
|
className="w-1.5 h-1.5 bg-stone-400 rounded-full"
|
||||||
animate={{ y: [0, -8, 0] }}
|
animate={{ y: [0, -6, 0] }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 0.6,
|
duration: 0.6,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
@@ -365,7 +365,7 @@ export default function ChatWidget() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input */}
|
{/* Input */}
|
||||||
<div className="p-3 md:p-4 bg-white/40 backdrop-blur-md border-t border-stone-200/50">
|
<div className="p-4 bg-white/30 backdrop-blur-xl border-t border-white/30">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@@ -375,37 +375,37 @@ export default function ChatWidget() {
|
|||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
placeholder="Ask anything..."
|
placeholder="Ask anything..."
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="flex-1 px-4 py-2.5 text-sm bg-white/80 backdrop-blur-sm text-stone-800 rounded-full border border-stone-200 focus:outline-none focus:ring-2 focus:ring-stone-200 focus:border-stone-400 focus:bg-white disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-stone-400 transition-all focus-visible:ring-stone-200 focus-visible:border-stone-400"
|
className="flex-1 px-4 py-3 text-sm bg-white/60 backdrop-blur-sm text-stone-800 rounded-xl border border-white/50 focus:outline-none focus:ring-2 focus:ring-stone-200/50 focus:border-stone-400/50 focus:bg-white/80 disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-stone-400 transition-all shadow-inner"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={!inputValue.trim() || isLoading}
|
disabled={!inputValue.trim() || isLoading}
|
||||||
className="p-2.5 bg-stone-800 text-stone-50 rounded-full hover:bg-stone-700 hover:shadow-lg hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 shadow-md"
|
className="p-3 bg-stone-800 text-stone-50 rounded-xl hover:bg-stone-700 hover:shadow-lg hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 shadow-md flex items-center justify-center aspect-square"
|
||||||
aria-label="Send message"
|
aria-label="Send message"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 size={20} className="animate-spin" />
|
<Loader2 size={18} className="animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<Send size={20} />
|
<Send size={18} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
<div className="flex gap-2 mt-2.5 overflow-x-auto pb-1 scrollbar-hide">
|
<div className="flex gap-2 mt-3 overflow-x-auto pb-1 scrollbar-hide mask-fade-right">
|
||||||
{[
|
{[
|
||||||
"What are Dennis's skills?",
|
"Skills 🛠️",
|
||||||
"Tell me about his projects",
|
"Projects 🚀",
|
||||||
"How can I contact him?",
|
"Contact 📧",
|
||||||
].map((suggestion, index) => (
|
].map((suggestion, index) => (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setInputValue(suggestion);
|
setInputValue(suggestion.replace(/ .*/, '')); // Strip emoji for search if needed, or keep
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="px-3 py-1.5 text-[10px] md:text-xs bg-white/60 backdrop-blur-sm text-stone-600 rounded-full hover:bg-white hover:text-stone-900 border border-stone-200 transition-all whitespace-nowrap disabled:opacity-50 flex-shrink-0 shadow-sm"
|
className="px-3 py-1.5 text-xs font-medium bg-white/40 backdrop-blur-md text-stone-600 rounded-lg hover:bg-white/70 hover:text-stone-900 border border-white/40 transition-all whitespace-nowrap disabled:opacity-50 flex-shrink-0 shadow-sm"
|
||||||
>
|
>
|
||||||
{suggestion}
|
{suggestion}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user