|
| 1 | +"use client"; |
| 2 | + |
| 3 | +import React, { useState, useEffect, useRef } from "react"; |
| 4 | +import { Button } from "@/components/ui/button"; |
| 5 | +import { Input } from "@/components/ui/input"; |
| 6 | +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; |
| 7 | +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; |
| 8 | +import { ScrollArea } from "@/components/ui/scroll-area"; |
| 9 | +import { Send } from "lucide-react"; |
| 10 | +import { io, Socket } from "socket.io-client"; |
| 11 | +import { useAuth } from "@/app/auth/auth-context"; |
| 12 | +import LoadingScreen from "@/components/common/loading-screen"; |
| 13 | + |
| 14 | +interface Message { |
| 15 | + id: string; |
| 16 | + userId: string; |
| 17 | + text: string; |
| 18 | + timestamp: Date; |
| 19 | +} |
| 20 | + |
| 21 | +export default function Chat({ roomId }: { roomId: string }) { |
| 22 | + const auth = useAuth(); |
| 23 | + const own_user_id = auth?.user?.id; |
| 24 | + const [socket, setSocket] = useState<Socket | null>(null); |
| 25 | + const [chatTarget, setChatTarget] = useState<string>("partner"); |
| 26 | + const [newMessage, setNewMessage] = useState<string>(""); |
| 27 | + const [partnerMessages, setPartnerMessages] = useState<Message[]>([]); |
| 28 | + const [aiMessages, setAiMessages] = useState<Message[]>([]); |
| 29 | + const [isConnected, setIsConnected] = useState(false); |
| 30 | + const lastMessageRef = useRef<HTMLDivElement | null>(null); |
| 31 | + |
| 32 | + useEffect(() => { |
| 33 | + if (!auth?.user?.id) return; // Avoid connecting if user is not authenticated |
| 34 | + |
| 35 | + const socketInstance = io( |
| 36 | + process.env.NEXT_PUBLIC_COLLAB_SERVICE_URL || "http://localhost:3002", |
| 37 | + { |
| 38 | + auth: { userId: own_user_id }, |
| 39 | + } |
| 40 | + ); |
| 41 | + |
| 42 | + socketInstance.on("connect", () => { |
| 43 | + console.log("Connected to Socket.IO"); |
| 44 | + setIsConnected(true); |
| 45 | + socketInstance.emit("joinRoom", roomId); |
| 46 | + }); |
| 47 | + |
| 48 | + socketInstance.on("disconnect", () => { |
| 49 | + console.log("Disconnected from Socket.IO"); |
| 50 | + setIsConnected(false); |
| 51 | + }); |
| 52 | + |
| 53 | + socketInstance.on("chatMessage", (message: Message) => { |
| 54 | + setPartnerMessages((prev) => [...prev, message]); |
| 55 | + }); |
| 56 | + |
| 57 | + setSocket(socketInstance); |
| 58 | + |
| 59 | + return () => { |
| 60 | + socketInstance.disconnect(); |
| 61 | + }; |
| 62 | + }, [roomId, own_user_id, auth?.user?.id]); |
| 63 | + |
| 64 | + useEffect(() => { |
| 65 | + const scrollWithDelay = () => { |
| 66 | + setTimeout(() => { |
| 67 | + if (lastMessageRef.current) { |
| 68 | + lastMessageRef.current.scrollIntoView({ behavior: "smooth" }); |
| 69 | + } |
| 70 | + }, 100); // Delay to ensure the DOM is fully rendered |
| 71 | + }; |
| 72 | + |
| 73 | + scrollWithDelay(); |
| 74 | + }, [partnerMessages, aiMessages, chatTarget]); |
| 75 | + |
| 76 | + const sendMessage = () => { |
| 77 | + if (!newMessage.trim() || !socket || !isConnected || !own_user_id) return; |
| 78 | + |
| 79 | + const message = { |
| 80 | + id: crypto.randomUUID(), |
| 81 | + userId: own_user_id, |
| 82 | + text: newMessage, |
| 83 | + timestamp: new Date(), |
| 84 | + }; |
| 85 | + |
| 86 | + if (chatTarget === "partner") { |
| 87 | + socket.emit("sendMessage", { |
| 88 | + roomId, |
| 89 | + userId: own_user_id, |
| 90 | + text: newMessage, |
| 91 | + }); |
| 92 | + } else { |
| 93 | + setAiMessages((prev) => [...prev, message]); |
| 94 | + } |
| 95 | + |
| 96 | + setNewMessage(""); |
| 97 | + }; |
| 98 | + |
| 99 | + const formatTimestamp = (date: Date) => { |
| 100 | + return new Date(date).toLocaleTimeString([], { |
| 101 | + hour: "2-digit", |
| 102 | + minute: "2-digit", |
| 103 | + }); |
| 104 | + }; |
| 105 | + |
| 106 | + const renderMessage = (message: Message, isOwnMessage: boolean) => ( |
| 107 | + <div |
| 108 | + key={message.id} |
| 109 | + className={`p-2 rounded-lg mb-2 max-w-[80%] ${ |
| 110 | + isOwnMessage |
| 111 | + ? "ml-auto bg-blue-500 text-white" |
| 112 | + : "bg-gray-100 dark:bg-gray-800" |
| 113 | + }`} |
| 114 | + > |
| 115 | + <div className="text-sm">{message.text}</div> |
| 116 | + <div |
| 117 | + className={`text-xs ${isOwnMessage ? "text-blue-100" : "text-gray-500"}`} |
| 118 | + > |
| 119 | + {formatTimestamp(message.timestamp)} |
| 120 | + </div> |
| 121 | + </div> |
| 122 | + ); |
| 123 | + |
| 124 | + if (!own_user_id) { |
| 125 | + return <LoadingScreen />; |
| 126 | + } |
| 127 | + |
| 128 | + return ( |
| 129 | + <Card className="flex flex-col"> |
| 130 | + <CardHeader> |
| 131 | + <CardTitle className="flex justify-between items-center"> |
| 132 | + Chat |
| 133 | + <span |
| 134 | + className={`h-2 w-2 rounded-full ${isConnected ? "bg-green-500" : "bg-red-500"}`} |
| 135 | + /> |
| 136 | + </CardTitle> |
| 137 | + </CardHeader> |
| 138 | + <CardContent className="flex-1 flex flex-col"> |
| 139 | + <Tabs |
| 140 | + value={chatTarget} |
| 141 | + onValueChange={setChatTarget} |
| 142 | + className="flex-col" |
| 143 | + > |
| 144 | + <TabsList className="flex-shrink-0 mb-2"> |
| 145 | + <TabsTrigger value="partner">Partner Chat</TabsTrigger> |
| 146 | + <TabsTrigger value="ai">AI Chat</TabsTrigger> |
| 147 | + </TabsList> |
| 148 | + <TabsContent value="partner" className="h-full"> |
| 149 | + <ScrollArea className="h-[calc(70vh-280px)]"> |
| 150 | + <div className="pr-4 space-y-2"> |
| 151 | + {partnerMessages.map((msg) => |
| 152 | + renderMessage(msg, msg.userId === own_user_id) |
| 153 | + )} |
| 154 | + <div ref={lastMessageRef} /> |
| 155 | + </div> |
| 156 | + </ScrollArea> |
| 157 | + </TabsContent> |
| 158 | + <TabsContent value="ai" className="h-full"> |
| 159 | + <ScrollArea className="h-[calc(70vh-280px)]"> |
| 160 | + <div className="pr-4 space-y-2"> |
| 161 | + {aiMessages.map((msg) => |
| 162 | + renderMessage(msg, msg.userId === own_user_id) |
| 163 | + )} |
| 164 | + <div ref={lastMessageRef} /> |
| 165 | + </div> |
| 166 | + </ScrollArea> |
| 167 | + </TabsContent> |
| 168 | + </Tabs> |
| 169 | + <div className="flex space-x-2 mt-4 pt-4 border-t"> |
| 170 | + <Input |
| 171 | + value={newMessage} |
| 172 | + onChange={(e) => setNewMessage(e.target.value)} |
| 173 | + placeholder={`Message ${chatTarget === "partner" ? "your partner" : "AI assistant"}...`} |
| 174 | + onKeyDown={(e) => e.key === "Enter" && sendMessage()} |
| 175 | + disabled={!isConnected} |
| 176 | + /> |
| 177 | + <Button onClick={sendMessage} disabled={!isConnected}> |
| 178 | + <Send className="h-4 w-4" /> |
| 179 | + </Button> |
| 180 | + </div> |
| 181 | + </CardContent> |
| 182 | + </Card> |
| 183 | + ); |
| 184 | +} |
0 commit comments