146 lines
6.0 KiB
TypeScript
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>
|
|
);
|
|
}; |