/* ┌──────────────────────────────────────────────────────────────────────────────┐ │ @author: Davidson Gomes │ │ @file: /app/chat/page.tsx │ │ Developed by: Davidson Gomes │ │ Creation date: May 13, 2025 │ │ Contact: contato@evolution-api.com │ ├──────────────────────────────────────────────────────────────────────────────┤ │ @copyright © Evolution API 2025. All rights reserved. │ │ Licensed under the Apache License, Version 2.0 │ │ │ │ You may not use this file except in compliance with the License. │ │ You may obtain a copy of the License at │ │ │ │ http://www.apache.org/licenses/LICENSE-2.0 │ │ │ │ Unless required by applicable law or agreed to in writing, software │ │ distributed under the License is distributed on an "AS IS" BASIS, │ │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ │ See the License for the specific language governing permissions and │ │ limitations under the License. │ ├──────────────────────────────────────────────────────────────────────────────┤ │ @important │ │ For any future changes to the code in this file, it is recommended to │ │ include, together with the modification, the information of the developer │ │ who changed it and the date of modification. │ └──────────────────────────────────────────────────────────────────────────────┘ */ "use client"; import { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { MessageSquare, Send, Plus, Search, Loader2, X, Trash2, Bot, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { useToast } from "@/hooks/use-toast"; import { Dialog, DialogContent, DialogTitle, DialogHeader, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { listAgents } from "@/services/agentService"; import { listSessions, getSessionMessages, ChatMessage, deleteSession, ChatSession, ChatPart } from "@/services/sessionService"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useAgentWebSocket } from "@/hooks/use-agent-webSocket"; import { getAccessTokenFromCookie } from "@/lib/utils"; import { ChatMessage as ChatMessageComponent } from "./components/ChatMessage"; import { SessionList } from "./components/SessionList"; import { ChatInput } from "./components/ChatInput"; import { FileData } from "@/lib/file-utils"; import { AgentInfoDialog } from "./components/AgentInfoDialog"; interface FunctionMessageContent { title: string; content: string; author?: string; } export default function Chat() { const [isLoading, setIsLoading] = useState(true); const [agents, setAgents] = useState([]); const [sessions, setSessions] = useState([]); const [messages, setMessages] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [agentSearchTerm, setAgentSearchTerm] = useState(""); const [selectedAgentFilter, setSelectedAgentFilter] = useState("all"); const [messageInput, setMessageInput] = useState(""); const [selectedSession, setSelectedSession] = useState(null); const [currentAgentId, setCurrentAgentId] = useState(null); const [isSending, setIsSending] = useState(false); const [isNewChatDialogOpen, setIsNewChatDialogOpen] = useState(false); const [showAgentFilter, setShowAgentFilter] = useState(false); const [expandedFunctions, setExpandedFunctions] = useState< Record >({}); const [isAgentInfoDialogOpen, setIsAgentInfoDialogOpen] = useState(false); const messagesContainerRef = useRef(null); const { toast } = useToast(); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const user = typeof window !== "undefined" ? JSON.parse(localStorage.getItem("user") || "{}") : {}; const clientId = user?.client_id || "test"; const scrollToBottom = () => { if (messagesContainerRef.current) { messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; } }; useEffect(() => { const loadData = async () => { setIsLoading(true); try { const agentsResponse = await listAgents(clientId); setAgents(agentsResponse.data); const sessionsResponse = await listSessions(clientId); setSessions(sessionsResponse.data); } catch (error) { console.error("Error loading data:", error); } finally { setIsLoading(false); } }; loadData(); }, [clientId]); useEffect(() => { if (!selectedSession) { setMessages([]); return; } const loadMessages = async () => { try { setIsLoading(true); const response = await getSessionMessages(selectedSession); setMessages(response.data); const agentId = selectedSession.split("_")[1]; setCurrentAgentId(agentId); setTimeout(scrollToBottom, 100); } catch (error) { console.error("Error loading messages:", error); } finally { setIsLoading(false); } }; loadMessages(); }, [selectedSession]); useEffect(() => { if (messages.length > 0) { setTimeout(scrollToBottom, 100); } }, [messages]); const filteredSessions = sessions.filter((session) => { const matchesSearchTerm = session.id .toLowerCase() .includes(searchTerm.toLowerCase()); if (selectedAgentFilter === "all") { return matchesSearchTerm; } const sessionAgentId = session.id.split("_")[1]; return matchesSearchTerm && sessionAgentId === selectedAgentFilter; }); const sortedSessions = [...filteredSessions].sort((a, b) => { const updateTimeA = new Date(a.update_time).getTime(); const updateTimeB = new Date(b.update_time).getTime(); return updateTimeB - updateTimeA; }); const formatDateTime = (dateTimeStr: string) => { try { const date = new Date(dateTimeStr); const day = date.getDate().toString().padStart(2, "0"); const month = (date.getMonth() + 1).toString().padStart(2, "0"); const year = date.getFullYear(); const hours = date.getHours().toString().padStart(2, "0"); const minutes = date.getMinutes().toString().padStart(2, "0"); return `${day}/${month}/${year} ${hours}:${minutes}`; } catch (error) { return "Invalid date"; } }; const filteredAgents = agents.filter( (agent) => agent.name.toLowerCase().includes(agentSearchTerm.toLowerCase()) || (agent.description && agent.description.toLowerCase().includes(agentSearchTerm.toLowerCase())) ); const selectAgent = (agentId: string) => { setCurrentAgentId(agentId); setSelectedSession(null); setMessages([]); setIsNewChatDialogOpen(false); }; const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault(); if (!messageInput.trim() || !currentAgentId) return; setIsSending(true); setMessages((prev) => [ ...prev, { id: `temp-${Date.now()}`, content: { parts: [{ text: messageInput }], role: "user", }, author: "user", timestamp: Date.now() / 1000, }, ]); wsSendMessage(messageInput); setMessageInput(""); const textarea = document.querySelector("textarea"); if (textarea) textarea.style.height = "auto"; }; const handleSendMessageWithFiles = (message: string, files?: FileData[]) => { if ((!message.trim() && (!files || files.length === 0)) || !currentAgentId) return; setIsSending(true); const messageParts: ChatPart[] = []; if (message.trim()) { messageParts.push({ text: message }); } if (files && files.length > 0) { files.forEach(file => { messageParts.push({ inline_data: { data: file.data, mime_type: file.content_type, metadata: { filename: file.filename } } }); }); } setMessages((prev) => [ ...prev, { id: `temp-${Date.now()}`, content: { parts: messageParts, role: "user" }, author: "user", timestamp: Date.now() / 1000, }, ]); wsSendMessage(message, files); setMessageInput(""); const textarea = document.querySelector("textarea"); if (textarea) textarea.style.height = "auto"; }; const generateExternalId = () => { const now = new Date(); return ( now.getFullYear().toString() + (now.getMonth() + 1).toString().padStart(2, "0") + now.getDate().toString().padStart(2, "0") + now.getHours().toString().padStart(2, "0") + now.getMinutes().toString().padStart(2, "0") + now.getSeconds().toString().padStart(2, "0") + now.getMilliseconds().toString().padStart(3, "0") ); }; const currentAgent = agents.find((agent) => agent.id === currentAgentId); const getCurrentSessionInfo = () => { if (!selectedSession) return null; const parts = selectedSession.split("_"); try { const dateStr = parts[0]; if (dateStr.length >= 8) { const year = dateStr.substring(0, 4); const month = dateStr.substring(4, 6); const day = dateStr.substring(6, 8); return { externalId: parts[0], agentId: parts[1], displayDate: `${day}/${month}/${year}`, }; } } catch (e) { console.error("Error processing session ID:", e); } return { externalId: parts[0], agentId: parts[1], displayDate: "Session", }; }; const getExternalId = (sessionId: string) => { return sessionId.split("_")[0]; }; const containsMarkdown = (text: string): boolean => { if (!text || text.length < 3) return false; const markdownPatterns = [ /[*_]{1,2}[^*_]+[*_]{1,2}/, // bold/italic /\[[^\]]+\]\([^)]+\)/, // links /^#{1,6}\s/m, // headers /^[-*+]\s/m, // unordered lists /^[0-9]+\.\s/m, // ordered lists /^>\s/m, // block quotes /`[^`]+`/, // inline code /```[\s\S]*?```/, // code blocks /^\|(.+\|)+$/m, // tables /!\[[^\]]*\]\([^)]+\)/, // images ]; return markdownPatterns.some((pattern) => pattern.test(text)); }; const getMessageText = ( message: ChatMessage ): string | FunctionMessageContent => { const author = message.author; const parts = message.content.parts; if (!parts || parts.length === 0) return "Empty content"; const functionCallPart = parts.find( (part) => part.functionCall || part.function_call ); const functionResponsePart = parts.find( (part) => part.functionResponse || part.function_response ); const inlineDataParts = parts.filter((part) => part.inline_data); if (functionCallPart) { const funcCall = functionCallPart.functionCall || functionCallPart.function_call || {}; const args = funcCall.args || {}; const name = funcCall.name || "unknown"; const id = funcCall.id || "no-id"; return { author, title: `📞 Function call: ${name}`, content: `ID: ${id} Args: ${ Object.keys(args).length > 0 ? `\n${JSON.stringify(args, null, 2)}` : "{}" }`, } as FunctionMessageContent; } if (functionResponsePart) { const funcResponse = functionResponsePart.functionResponse || functionResponsePart.function_response || {}; const response = funcResponse.response || {}; const name = funcResponse.name || "unknown"; const id = funcResponse.id || "no-id"; const status = response.status || "unknown"; const statusEmoji = status === "error" ? "❌" : "✅"; let resultText = ""; if (status === "error") { resultText = `Error: ${response.error_message || "Unknown error"}`; } else if (response.report) { resultText = `Result: ${response.report}`; } else if (response.result && response.result.content) { const content = response.result.content; if (Array.isArray(content) && content.length > 0 && content[0].text) { try { const textContent = content[0].text; const parsedJson = JSON.parse(textContent); resultText = `Result: \n${JSON.stringify(parsedJson, null, 2)}`; } catch (e) { resultText = `Result: ${content[0].text}`; } } else { resultText = `Result:\n${JSON.stringify(response, null, 2)}`; } } else { resultText = `Result:\n${JSON.stringify(response, null, 2)}`; } return { author, title: `${statusEmoji} Function response: ${name}`, content: `ID: ${id}\n${resultText}`, } as FunctionMessageContent; } if (parts.length === 1 && parts[0].text) { return { author, content: parts[0].text, title: "Message", } as FunctionMessageContent; } const textParts = parts .filter((part) => part.text) .map((part) => part.text) .filter((text) => text); if (textParts.length > 0) { return { author, content: textParts.join("\n\n"), title: "Message", } as FunctionMessageContent; } return "Empty content"; }; const toggleFunctionExpansion = (messageId: string) => { setExpandedFunctions((prev) => ({ ...prev, [messageId]: !prev[messageId], })); }; const agentColors: Record = { Assistant: "bg-emerald-400", Programmer: "bg-[#00cc7d]", Writer: "bg-[#00b8ff]", Researcher: "bg-[#ff9d00]", Planner: "bg-[#9d00ff]", default: "bg-[#333]", }; const getAgentColor = (agentName: string) => { return agentColors[agentName] || agentColors.default; }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSendMessage(e as unknown as React.FormEvent); } }; const autoResizeTextarea = (e: React.ChangeEvent) => { const textarea = e.target; textarea.style.height = "auto"; const maxHeight = 10 * 24; const newHeight = Math.min(textarea.scrollHeight, maxHeight); textarea.style.height = `${newHeight}px`; setMessageInput(textarea.value); }; const handleDeleteSession = async () => { if (!selectedSession) return; try { await deleteSession(selectedSession); setSessions(sessions.filter((session) => session.id !== selectedSession)); setSelectedSession(null); setMessages([]); setCurrentAgentId(null); setIsDeleteDialogOpen(false); toast({ title: "Session deleted successfully", }); } catch (error) { console.error("Error deleting session:", error); toast({ title: "Error deleting session", variant: "destructive", }); } }; const onEvent = useCallback((event: any) => { setMessages((prev) => [...prev, event]); }, []); const onTurnComplete = useCallback(() => { setIsSending(false); }, []); const handleAgentInfoClick = () => { setIsAgentInfoDialogOpen(true); }; const handleAgentUpdated = (updatedAgent: any) => { setAgents(agents.map(agent => agent.id === updatedAgent.id ? updatedAgent : agent )); toast({ title: "Agent updated successfully", description: "The agent has been updated with the new settings.", }); }; const jwt = getAccessTokenFromCookie(); const agentId = useMemo(() => currentAgentId || "", [currentAgentId]); const externalId = useMemo( () => selectedSession ? getExternalId(selectedSession) : generateExternalId(), [selectedSession] ); const { sendMessage: wsSendMessage, disconnect: _ } = useAgentWebSocket({ agentId, externalId, jwt, onEvent, onTurnComplete, }); return (
{selectedSession || currentAgentId ? ( <>
{(() => { const sessionInfo = getCurrentSessionInfo(); return (

{selectedSession ? `Session ${ sessionInfo?.externalId || selectedSession }` : "New Conversation"}

{currentAgent && ( {currentAgent.name || currentAgentId} )} {selectedSession && ( )}
); })()}
{isLoading ? (

Loading conversation...

) : messages.length === 0 ? (

{currentAgent ? `Chat with ${currentAgent.name}` : "New Conversation"}

Type your message below to start the conversation. This chat will help you interact with the agent and explore its capabilities.

) : (
{messages.map((message) => { const messageContent = getMessageText(message); const agentColor = getAgentColor(message.author); const isExpanded = expandedFunctions[message.id] || false; return ( ); })} {isSending && (
)}
)}
{isSending && !isLoading && (
Agent is thinking...
)}
) : (

Select a conversation

Choose an existing conversation or start a new one.

)}
New Conversation
Select an agent to start a new conversation.
setAgentSearchTerm(e.target.value)} /> {agentSearchTerm && ( )}
Choose an agent:
{isLoading ? (

Loading agents...

) : filteredAgents.length > 0 ? (
{filteredAgents.map((agent) => (
selectAgent(agent.id)} >
{agent.name}
{agent.type} {agent.model && ( {agent.model} )}
{agent.description && (

{agent.description}

)}
))}
) : agentSearchTerm ? (
No agent found with "{agentSearchTerm}"
) : (

No agents available

Create agents in the Agent Management screen

)}
Delete Session
Are you sure you want to delete this session? This action cannot be undone.
{/* Agent Info Dialog */}
); }