| | "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; |