fitmate-landing/components/ChatDemo.tsx
2025-11-20 22:22:22 +07:00

146 lines
6.0 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
const SEQUENCE = [
{ type: 'user', text: "I messed up. Ate half a pizza. What now?", delay: 500 },
{ type: 'thinking', duration: 1500 },
{ type: 'ai', text: "No problem. That happens.", delay: 500 },
{ type: 'ai', text: "I've adjusted tomorrow's plan: we'll drop fats slightly (-15g) to balance the weekly average. Enjoy your night! 🌙", delay: 1000 },
{ type: 'reset', delay: 4000 }
];
export const ChatDemo: React.FC = () => {
const [messages, setMessages] = useState<any[]>([]);
const [isTyping, setIsTyping] = useState(false);
useEffect(() => {
// Use ReturnType<typeof setTimeout> to ensure compatibility with both browser (number) and Node (Timeout) environments without requiring @types/node
let timeoutIds: ReturnType<typeof setTimeout>[] = [];
let mounted = true;
const runSequence = async () => {
if (!mounted) return;
setMessages([]);
setIsTyping(false);
let accumulatedDelay = 0;
SEQUENCE.forEach((step) => {
accumulatedDelay += (step.delay || 0) + (step.duration || 0);
const id = setTimeout(() => {
if (!mounted) return;
if (step.type === 'reset') {
runSequence();
return;
}
if (step.type === 'thinking') {
setIsTyping(true);
setTimeout(() => { if(mounted) setIsTyping(false) }, step.duration);
return;
}
if (step.type === 'user' || step.type === 'ai') {
setMessages(prev => [...prev, { id: Date.now(), role: step.type, text: step.text }]);
}
}, accumulatedDelay);
timeoutIds.push(id);
});
};
runSequence();
return () => {
mounted = false;
timeoutIds.forEach(clearTimeout);
};
}, []);
return (
<section className="py-32 bg-background border-y border-white/5 relative overflow-hidden">
{/* Ambient Glow */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[400px] bg-primary-500/5 blur-[120px] rounded-full pointer-events-none" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid lg:grid-cols-2 gap-16 items-center">
{/* Copy */}
<div>
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-white/5 border border-white/10 mb-8">
<span className="text-xs font-bold text-white tracking-wide uppercase">
Judgment-Free Zone
</span>
</div>
<h2 className="text-4xl md:text-6xl font-bold text-white mb-8 tracking-tighter">
A missed meal isn't a moral failure. <br/>
<span className="text-gray-600">It's just data.</span>
</h2>
<p className="text-lg text-gray-400 mb-8 leading-relaxed max-w-md">
Life happens. FitMate's <strong className="text-white">Rolling Adherence Buffer</strong> catches you when you fall.
The system simply recalculates the optimal path forward using the remaining days in your week.
</p>
</div>
{/* Chat Simulation */}
<div className="relative mx-auto w-full max-w-md">
<div className="bg-surface border border-white/10 rounded-3xl shadow-2xl overflow-hidden min-h-[500px] flex flex-col relative z-10">
{/* Header */}
<div className="px-6 py-5 border-b border-white/5 bg-surfaceHighlight/50 backdrop-blur flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-sm font-bold text-white">FitMate Co-Pilot</span>
</div>
</div>
{/* Messages Area */}
<div className="flex-1 p-6 space-y-4 flex flex-col justify-end">
<AnimatePresence mode='popLayout'>
{messages.map((msg) => (
<motion.div
key={msg.id}
initial={{ opacity: 0, y: 20, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
layout
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[85%] p-4 rounded-2xl text-sm leading-relaxed shadow-sm ${
msg.role === 'user'
? 'bg-white text-black rounded-tr-sm font-medium'
: 'bg-white/10 text-gray-200 rounded-tl-sm backdrop-blur-md'
}`}
>
{msg.text}
</div>
</motion.div>
))}
</AnimatePresence>
{/* Typing Indicator */}
{isTyping && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex justify-start"
>
<div className="bg-white/5 px-4 py-3 rounded-2xl rounded-tl-sm flex gap-1">
<span className="w-1.5 h-1.5 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}/>
<span className="w-1.5 h-1.5 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}/>
<span className="w-1.5 h-1.5 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}/>
</div>
</motion.div>
)}
</div>
</div>
</div>
</div>
</div>
</section>
);
};