|
|
"use client"; |
|
|
|
|
|
import React, { useState, useEffect, useRef } from 'react'; |
|
|
import Image from 'next/image'; |
|
|
import ChatMessage from './ChatMessage'; |
|
|
import ChatInput from './ChatInput'; |
|
|
|
|
|
type Message = { |
|
|
role: 'user' | 'assistant'; |
|
|
content: string; |
|
|
}; |
|
|
|
|
|
|
|
|
const generateMeowCount = () => { |
|
|
|
|
|
return Math.floor(Math.random() * 30) + 1; |
|
|
}; |
|
|
|
|
|
const Chat: React.FC = () => { |
|
|
const [messages, setMessages] = useState<Message[]>([ |
|
|
{ |
|
|
role: 'assistant', |
|
|
content: 'meow', |
|
|
}, |
|
|
]); |
|
|
const [isTyping, setIsTyping] = useState(false); |
|
|
const [streamingContent, setStreamingContent] = useState(''); |
|
|
const messagesEndRef = useRef<HTMLDivElement>(null); |
|
|
const streamIntervalRef = useRef<NodeJS.Timeout | null>(null); |
|
|
|
|
|
const scrollToBottom = () => { |
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); |
|
|
}; |
|
|
|
|
|
useEffect(() => { |
|
|
scrollToBottom(); |
|
|
}, [messages, isTyping, streamingContent]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
return () => { |
|
|
if (streamIntervalRef.current) { |
|
|
clearInterval(streamIntervalRef.current); |
|
|
} |
|
|
}; |
|
|
}, []); |
|
|
|
|
|
const handleSendMessage = (content: string) => { |
|
|
|
|
|
if (streamIntervalRef.current) { |
|
|
clearInterval(streamIntervalRef.current); |
|
|
streamIntervalRef.current = null; |
|
|
} |
|
|
|
|
|
|
|
|
setMessages((prev) => [...prev, { role: 'user', content }]); |
|
|
|
|
|
|
|
|
setIsTyping(true); |
|
|
setStreamingContent(''); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
const meowCount = generateMeowCount(); |
|
|
let currentMeows = 0; |
|
|
const meowArray = Array(meowCount).fill("meow"); |
|
|
|
|
|
|
|
|
streamIntervalRef.current = setInterval(() => { |
|
|
if (currentMeows < meowCount) { |
|
|
currentMeows++; |
|
|
setStreamingContent(meowArray.slice(0, currentMeows).join(" ")); |
|
|
} else { |
|
|
|
|
|
if (streamIntervalRef.current) { |
|
|
clearInterval(streamIntervalRef.current); |
|
|
streamIntervalRef.current = null; |
|
|
} |
|
|
|
|
|
|
|
|
setMessages((prev) => [...prev, { |
|
|
role: 'assistant', |
|
|
content: meowArray.join(" ") |
|
|
}]); |
|
|
|
|
|
setIsTyping(false); |
|
|
setStreamingContent(''); |
|
|
} |
|
|
}, 100); |
|
|
}, 500 + Math.random() * 500); |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div className="flex flex-col h-full bg-white"> |
|
|
<div className="flex-1 overflow-y-auto pb-4"> |
|
|
<div> |
|
|
{messages.map((message, index) => ( |
|
|
<ChatMessage key={index} role={message.role} content={message.content} /> |
|
|
))} |
|
|
{isTyping && ( |
|
|
<div className="py-6 bg-white"> |
|
|
<div className="max-w-3xl mx-auto flex items-start gap-4 px-4 sm:px-6 md:px-8"> |
|
|
<div className="flex-shrink-0 w-8 h-8"> |
|
|
<div className="w-8 h-8 flex items-center justify-center overflow-hidden"> |
|
|
<Image src="/cat.png" alt="CatGPT Logo" width={32} height={32} /> |
|
|
</div> |
|
|
</div> |
|
|
<div className="flex-1 min-w-0"> |
|
|
<p className="font-medium text-sm mb-2 text-gray-800">CatGPT</p> |
|
|
{streamingContent ? ( |
|
|
<div className="prose max-w-none text-gray-800"> |
|
|
<p className="whitespace-pre-wrap">{streamingContent}</p> |
|
|
</div> |
|
|
) : ( |
|
|
<div className="flex space-x-2 items-center"> |
|
|
<span className="w-2 h-2 rounded-full bg-gray-400 animate-pulse"></span> |
|
|
<span className="w-2 h-2 rounded-full bg-gray-400 animate-pulse" style={{ animationDelay: '0.2s' }}></span> |
|
|
<span className="w-2 h-2 rounded-full bg-gray-400 animate-pulse" style={{ animationDelay: '0.4s' }}></span> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
<div ref={messagesEndRef} /> |
|
|
</div> |
|
|
</div> |
|
|
<div className="mt-auto"> |
|
|
<ChatInput onSendMessage={handleSendMessage} disabled={isTyping} /> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default Chat; |