"use client";
import { LoadingOutlined, RobotOutlined, SendOutlined, UserOutlined } from "@ant-design/icons";
import { Avatar, Button, Input, Spin, Tag } from "antd";
import { useEffect, useRef, useState } from "react";
import { io, Socket } from "socket.io-client";
const NEST_BACKEND_URL = process.env.NEXT_PUBLIC_SOCKET_URL;
interface Message {
id: string;
role: "user" | "ai";
content: string;
steps?: string[];
}
export default function ChatPage() {
const [conversations, setConversations] = useState([]);
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState("");
const [isTyping, setIsTyping] = useState(false);
const [currentStep, setCurrentStep] = useState("");
const tempSteps = useRef<string[]>([]);
const socketRef = useRef<Socket | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
fetch(`${NEST_BACKEND_URL}/conversations`)
.then(res => res.json())
.then(setConversations)
.catch(err => console.error("Fetch error:", err));
socketRef.current = io(NEST_BACKEND_URL, {
path: "/socket-io-ai",
transports: ["websocket"],
});
socketRef.current.on("ai_processing", (data: { step: string }) => {
setIsTyping(true);
setCurrentStep(data.step);
tempSteps.current.push(data.step);
});
socketRef.current.on("ai_response", (data: Message) => {
const finalMsg: Message = {
...data,
steps: [...tempSteps.current],
};
setMessages(prev => [...prev, finalMsg]);
setIsTyping(false);
setCurrentStep("");
tempSteps.current = [];
});
return () => {
socketRef.current?.disconnect();
};
}, []);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: "smooth",
});
}
}, [messages, isTyping, currentStep]);
const handleSend = () => {
if (!inputValue.trim()) return;
const userMsg: Message = {
id: Date.now().toString(),
role: "user",
content: inputValue,
};
setMessages(prev => [...prev, userMsg]);
socketRef.current?.emit("sendMessage", { sender: "User", text: inputValue });
setInputValue("");
tempSteps.current = [];
};
return (
<div className="flex h-screen bg-slate-50 p-4 gap-4 overflow-hidden font-sans text-slate-900">
<div className="w-80 bg-white rounded-2xl shadow-sm border border-slate-200 flex flex-col overflow-hidden">
<div className="p-5 font-bold text-lg border-b border-slate-100 text-blue-600 bg-white uppercase tracking-wider">Conversations</div>
<div className="flex-1 overflow-y-auto custom-scrollbar">
{conversations.map((item: any) => (
<div key={item.id} className="p-4 hover:bg-blue-50 cursor-pointer border-b border-slate-50 flex gap-3 items-center group transition-colors">
<div className="flex-shrink-0">
<Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${item.id}`} size={45} className="border border-slate-100" />
</div>
<div className="flex-1 overflow-hidden">
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-slate-700 truncate">{item.name}</span>
<span className="text-[10px] text-slate-400 font-medium">{item.time}</span>
</div>
<div className="text-xs text-slate-500 truncate group-hover:text-blue-500 transition-colors">{item.lastMsg}</div>
</div>
</div>
))}
</div>
</div>
<div className="flex-1 bg-white rounded-2xl shadow-md border border-slate-200 flex flex-col overflow-hidden">
<div className="p-4 border-b border-slate-100 flex justify-between items-center bg-white/80 backdrop-blur-sm z-10">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<Avatar icon={<RobotOutlined />} className="bg-blue-600 shadow-sm" size={40} />
</div>
<div>
<div className="font-bold text-slate-800 leading-tight italic">AI Agentic Node</div>
<div className="text-[10px] text-blue-500 font-bold uppercase tracking-widest">Processing Online</div>
</div>
</div>
<Tag color="blue" className="rounded-full px-3 border-none bg-blue-50 text-blue-600 font-medium uppercase text-[10px]">
Socket.io Stream
</Tag>
</div>
<div ref={scrollRef} className="flex-1 overflow-y-auto p-6 space-y-8 bg-slate-50/50">
{messages.map(msg => (
<div key={msg.id} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"} animate-in fade-in duration-300`}>
<div className={`flex max-w-[85%] gap-3 ${msg.role === "user" ? "flex-row-reverse" : "flex-row"}`}>
<div className="flex-shrink-0 self-end mb-1">
<Avatar icon={msg.role === "user" ? <UserOutlined /> : <RobotOutlined />} className={`${msg.role === "user" ? "bg-slate-800" : "bg-blue-600"} shadow-sm`} size={32} />
</div>
<div className="flex flex-col gap-4">
{msg.role === "ai" && msg.steps && msg.steps.length > 0 && (
<div className="flex flex-col gap-2 ml-1">
<div className="flex items-center gap-2 opacity-60">
<div className="h-[1px] w-4 bg-slate-300"></div>
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Chain of Thought</span>
</div>
<div className="flex flex-col gap-1.5 border-l-2 border-slate-100 ml-1 pl-4">
{msg.steps.map((step, idx) => (
<div key={idx} className="flex items-center gap-3 py-0.5 group">
<div className="w-1.5 h-1.5 rounded-full bg-blue-400/50 group-last:bg-blue-500 shadow-[0_0_8px_rgba(96,165,250,0.5)]"></div>
<span className="text-[12px] text-slate-500 italic font-medium leading-none tracking-tight">{step}</span>
</div>
))}
</div>
</div>
)}
<div
className={`p-4 rounded-2xl shadow-sm text-[14.5px] leading-relaxed tracking-tight ${
msg.role === "user" ? "bg-blue-600 text-white rounded-br-none shadow-blue-100" : "bg-white text-slate-700 border border-slate-100 rounded-tl-none font-medium"
}`}
>
{msg.content}
</div>
</div>
</div>
</div>
))}
{isTyping && (
<div className="flex justify-start gap-3 pl-11">
<div className="bg-white/80 backdrop-blur-sm px-4 py-2 rounded-2xl border border-blue-100 flex items-center gap-3 shadow-sm ring-4 ring-blue-50/50">
<span className="text-[12px] text-blue-600 font-bold italic tracking-wide animate-pulse">{currentStep || "Analyzing..."}</span>
<Spin indicator={<LoadingOutlined className="text-blue-600 text-sm" spin />} />
</div>
</div>
)}
</div>
<div className="p-5 bg-white border-t border-slate-100">
<div className="flex gap-2 p-1.5 border border-slate-200 rounded-2xl bg-slate-50 focus-within:bg-white focus-within:border-blue-500 focus-within:ring-4 focus-within:ring-blue-100/50 transition-all shadow-inner">
<Input
variant="borderless"
placeholder="Ask AI anything..."
value={inputValue}
onChange={e => setInputValue(e.target.value)}
onPressEnter={handleSend}
disabled={isTyping}
className="py-2 px-4 text-slate-700 placeholder:text-slate-400"
/>
<Button
type="primary"
size="large"
shape="circle"
icon={<SendOutlined />}
onClick={handleSend}
disabled={!inputValue.trim() || isTyping}
className="flex-shrink-0 shadow-lg shadow-blue-200 flex items-center justify-center bg-blue-600 hover:scale-105 active:scale-95 transition-transform"
/>
</div>
</div>
</div>
</div>
);
}
Comments
Post a Comment