"use client" import type React from "react" import { useState, useRef, useEffect } from "react" import { motion, AnimatePresence } from "framer-motion" import { Button } from "@/components/ui/button" import { Send, Loader2, SquarePen, Paperclip, Copy, X, ChevronDown, Bookmark } from "lucide-react" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { MarkdownRenderer } from "./markdown-renderer" import { AgentForgeCard } from "./agent-forge-card" import { PinnedAgentsDrawer } from "./pinned-agents-drawer" import type { Message, Agent, AgentPackagePayload, PinnedAgent, ToolCall } from "@/lib/types" import { cn } from "@/lib/utils" import { useFlags } from "@/lib/use-flags" interface ChatInterfaceProps { agent: Agent agents: Agent[] onAgentSelected: (agent: Agent) => void isAgentsLoading: boolean } export function ChatInterface({ agent, agents, onAgentSelected, isAgentsLoading, }: ChatInterfaceProps) { const heroGreeting = "hello, user" const [messages, setMessages] = useState([]) const [input, setInput] = useState("") const [isLoading, setIsLoading] = useState(false) const [sessionId, setSessionId] = useState("") const [selectedImages, setSelectedImages] = useState([]) const [composerAgentId, setComposerAgentId] = useState(null) const [textareaHeight, setTextareaHeight] = useState(32) const messagesContainerRef = useRef(null) const inputRef = useRef(null) const fileInputRef = useRef(null) const [copiedMessageId, setCopiedMessageId] = useState(null) const { flags } = useFlags() const [agentPackage, setAgentPackage] = useState(null) const [showPinnedDrawer, setShowPinnedDrawer] = useState(false) const [pinnedAgents, setPinnedAgents] = useState(() => { // Initialize from localStorage to avoid flicker on mount if (typeof window !== "undefined") { const stored = localStorage.getItem("pinned-agents") if (stored) { try { return JSON.parse(stored) } catch (error) { console.error("Failed to parse pinned agents:", error) return [] } } } return [] }) const [morganAnimating, setMorganAnimating] = useState(false) useEffect(() => { // Use agent-specific session ID: chat-session-{agentId} const sessionKey = `chat-session-${agent.id}` let existingSessionId = localStorage.getItem(sessionKey) if (!existingSessionId) { // Generate new sessionID using timestamp and random string existingSessionId = `session-${agent.id}-${Date.now()}-${Math.random().toString(36).substring(2, 15)}` localStorage.setItem(sessionKey, existingSessionId) } setSessionId(existingSessionId) // Load existing messages for this agent const messagesKey = `chat-messages-${agent.id}` const savedMessages = localStorage.getItem(messagesKey) if (savedMessages) { try { const parsed = JSON.parse(savedMessages) // Ensure timestamps are Date objects const messages = parsed.map((msg: any) => ({ ...msg, timestamp: new Date(msg.timestamp), })) setMessages(messages) } catch (err) { console.error("[chat] Failed to load saved messages:", err) } } }, [agent.id]) useEffect(() => { if (messagesContainerRef.current) { messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight } }, [messages, isLoading]) // Update textarea height based on content useEffect(() => { if (inputRef.current) { const element = inputRef.current element.style.height = "auto" const newHeight = Math.min(element.scrollHeight, 224) setTextareaHeight(newHeight) } }, [input]) // Save messages to localStorage whenever they change useEffect(() => { const messagesKey = `chat-messages-${agent.id}` localStorage.setItem(messagesKey, JSON.stringify(messages)) }, [messages, agent.id]) useEffect(() => { if (inputRef.current) { inputRef.current.style.height = "auto" inputRef.current.style.height = Math.min(inputRef.current.scrollHeight, 160) + "px" } }, [input]) useEffect(() => { if (messages.length > 0 && composerAgentId !== agent.id) { setComposerAgentId(agent.id) } }, [messages.length, agent.id]) // Handle image file selection const handleImageSelect = async (e: React.ChangeEvent) => { const files = e.currentTarget.files if (!files) return const newImages: string[] = [] for (let i = 0; i < files.length; i++) { const file = files[i] // Only accept image files if (!file.type.startsWith("image/")) { console.warn("[chat] Skipping non-image file:", file.name) continue } try { const base64 = await fileToBase64(file) newImages.push(base64) } catch (err) { console.error("[chat] Failed to convert image:", err) } } setSelectedImages((prev) => [...prev, ...newImages]) // Reset file input if (fileInputRef.current) { fileInputRef.current.value = "" } } // Convert file to base64 string const fileToBase64 = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader() reader.readAsDataURL(file) reader.onload = () => resolve(reader.result as string) reader.onerror = reject }) } // Remove selected image const removeImage = (index: number) => { setSelectedImages((prev) => prev.filter((_, i) => i !== index)) } const sendMessage = async (e?: React.FormEvent) => { if (e) { e.preventDefault() } if (!input.trim() || isLoading) return const userMessage: Message = { id: Date.now().toString(), role: "user", content: input.trim(), timestamp: new Date(), images: selectedImages.length > 0 ? selectedImages : undefined, } setMessages((prev) => [...prev, userMessage]) setInput("") setSelectedImages([]) setIsLoading(true) try { const response = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ message: userMessage.content, timestamp: userMessage.timestamp.toISOString(), sessionId: sessionId, agentId: agent.id, images: selectedImages.length > 0 ? selectedImages : undefined, }), }) const data = (await response.json()) as { error?: string hint?: string response?: string message?: string toolCall?: ToolCall } if (!response.ok) { const errorMessage: Message = { id: (Date.now() + 1).toString(), role: "assistant", content: data.error || "Failed to communicate with the webhook.", timestamp: new Date(), isError: true, hint: data.hint, } setMessages((prev) => [...prev, errorMessage]) } else { // Check if this is a tool call (e.g., agent package creation) if (data.toolCall && data.toolCall.name === "create_agent_package") { const payload = data.toolCall.payload as AgentPackagePayload setAgentPackage(payload) // Don't add a regular message, the AgentForgeCard will be rendered instead } else { const assistantMessage: Message = { id: (Date.now() + 1).toString(), role: "assistant", content: data.response || data.message || JSON.stringify(data), timestamp: new Date(), } setMessages((prev) => [...prev, assistantMessage]) } } } catch (error) { console.error("[v0] Error sending message:", error) const errorMessage: Message = { id: (Date.now() + 1).toString(), role: "assistant", content: "Sorry, I encountered an error processing your message. Please try again.", timestamp: new Date(), isError: true, } setMessages((prev) => [...prev, errorMessage]) } finally { setIsLoading(false) inputRef.current?.focus() } } const startNewChat = () => { // Clear all messages setMessages([]) // Generate new sessionID for this agent const newSessionId = `session-${agent.id}-${Date.now()}-${Math.random().toString(36).substring(2, 15)}` setSessionId(newSessionId) const sessionKey = `chat-session-${agent.id}` localStorage.setItem(sessionKey, newSessionId) // Clear input and images setInput("") setSelectedImages([]) setComposerAgentId(null) // Focus input inputRef.current?.focus() } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() sendMessage() } } const handleCopyMessage = async (id: string, content: string) => { try { await navigator.clipboard.writeText(content) setCopiedMessageId(id) setTimeout(() => { setCopiedMessageId((current) => (current === id ? null : current)) }, 1200) } catch (error) { console.error("[chat] Failed to copy message", error) } } // Handle agent package actions const handleUseAgentNow = async (agentId: string) => { if (!agentPackage) return // Register the agent with the backend try { const response = await fetch("/api/agents/create", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ agentId, systemPrompt: agentPackage.systemPrompt, metadata: { displayName: agentPackage.displayName, summary: agentPackage.summary, tags: agentPackage.tags, recommendedIcon: agentPackage.hints?.recommendedIcon, whenToUse: agentPackage.hints?.whenToUse, }, }), }) if (!response.ok) { console.error("Failed to register agent") return } // Create a temporary agent object and switch to it const customAgent: Agent = { id: agentId, name: agentPackage.displayName, description: agentPackage.summary, webhookUrl: "", // Will be handled by custom webhook } onAgentSelected(customAgent) setAgentPackage(null) // Add a timeline marker const marker: Message = { id: Date.now().toString(), role: "assistant", content: `✓ Now chatting with **${agentPackage.displayName}**`, timestamp: new Date(), } setMessages((prev) => [...prev, marker]) } catch (error) { console.error("Error registering agent:", error) } } const handlePinAgent = (pkg: AgentPackagePayload, note?: string) => { const pinnedAgent: PinnedAgent = { agentId: pkg.agentId, displayName: pkg.displayName, summary: pkg.summary, tags: pkg.tags, recommendedIcon: pkg.hints?.recommendedIcon, whenToUse: pkg.hints?.whenToUse, systemPrompt: pkg.systemPrompt, pinnedAt: new Date().toISOString(), note, } // Add to pinned agents in localStorage const stored = localStorage.getItem("pinned-agents") const existing = stored ? JSON.parse(stored) : [] const updated = [...existing, pinnedAgent] localStorage.setItem("pinned-agents", JSON.stringify(updated)) // Show confirmation const confirmation: Message = { id: Date.now().toString(), role: "assistant", content: `✓ **${pkg.displayName}** pinned for later use`, timestamp: new Date(), } setMessages((prev) => [...prev, confirmation]) setAgentPackage(null) } const handleShareAgent = async (agentId: string) => { if (!agentPackage) return // Create a shareable link or copy agent ID const shareText = `Check out this custom agent: ${agentPackage.displayName}\nAgent ID: ${agentId}` try { await navigator.clipboard.writeText(shareText) // Could also generate a deep link here } catch (error) { console.error("Failed to copy share link:", error) } } const handleSelectPinnedAgent = async (pinnedAgent: PinnedAgent) => { // Register with backend if not already registered try { await fetch("/api/agents/create", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ agentId: pinnedAgent.agentId, systemPrompt: pinnedAgent.systemPrompt, metadata: { displayName: pinnedAgent.displayName, summary: pinnedAgent.summary, tags: pinnedAgent.tags, recommendedIcon: pinnedAgent.recommendedIcon, whenToUse: pinnedAgent.whenToUse, }, }), }) } catch (error) { console.error("Error registering pinned agent:", error) } // Switch to this agent const customAgent: Agent = { id: pinnedAgent.agentId, name: pinnedAgent.displayName, description: pinnedAgent.summary, webhookUrl: "", } onAgentSelected(customAgent) } const handleComposerAgentSelect = (entry: Agent) => { setComposerAgentId(entry.id) onAgentSelected(entry) } const canSwitchAgents = agents.length > 0 && !isAgentsLoading const hasMessages = messages.length > 0 const dropdownSelectedId = composerAgentId ?? (hasMessages ? agent.id : null) const dropdownAgentEntry = dropdownSelectedId ? agents.find((entry) => entry.id === dropdownSelectedId) ?? agent : null const dropdownLabel = dropdownAgentEntry ? dropdownAgentEntry.name : "Select a correspondent" const highlightAgentDropdown = !dropdownSelectedId && !hasMessages return (
{/* Pinned agents drawer - rendered first so it's behind the chat panel */} setShowPinnedDrawer(false)} onSelectAgent={handleSelectPinnedAgent} />
{messages.length > 0 && ( )}
{hasMessages ? ( {messages.map((message) => { const isUser = message.role === "user" return ( {isUser ? (
) : message.isError ? (

{message.content}

) : (
)} {message.hint && (
{message.hint}
)}
) })} {isLoading && (
Correspondent
)} {agentPackage && ( )}
) : (

{heroGreeting.split("").map((char, index) => ( {char === " " ? "\u00A0" : char} ))}

Select a correspondent to begin

{agents.length > 0 ? (
{agents.map((entry, index) => { const isActive = dropdownSelectedId === entry.id return ( handleComposerAgentSelect(entry)} className={cn( "rounded-full px-4 py-2 text-[0.65rem] uppercase tracking-[0.35em] transition relative overflow-hidden group backdrop-blur-sm shadow-[0_2px_8px_rgba(0,0,0,0.15)]", isActive ? "bg-white/20 text-white shadow-[0_4px_12px_rgba(0,0,0,0.25)]" : "bg-white/8 text-white/70 hover:bg-white/15 hover:text-white" )} > {!isActive && (
)} {entry.name} ) })} {pinnedAgents.slice(0, 2).map((pinnedAgent, index) => { const isActive = dropdownSelectedId === pinnedAgent.agentId return ( handleComposerAgentSelect({ id: pinnedAgent.agentId, name: pinnedAgent.displayName, description: pinnedAgent.summary || "", webhookUrl: "" // Custom agents use dynamic routing } as Agent)} className={cn( "rounded-full px-4 py-2 text-[0.65rem] uppercase tracking-[0.35em] transition relative overflow-hidden group backdrop-blur-sm shadow-[0_2px_8px_rgba(0,0,0,0.15)]", isActive ? "bg-white/15 text-white shadow-[0_4px_12px_rgba(0,0,0,0.25)]" : "bg-white/8 text-white/70 hover:bg-white/15 hover:text-white" )} > {!isActive && (
)} {pinnedAgent.recommendedIcon} {pinnedAgent.displayName} ) })} { setMorganAnimating(true) setTimeout(() => { handleComposerAgentSelect(agents.find(a => a.name === "Morgan") || agents[0]) setInput("Help me create a new custom agent") setMorganAnimating(false) }, 600) }} className="rounded-full bg-white/8 px-4 py-2 text-[0.65rem] uppercase tracking-[0.35em] text-white/70 transition relative overflow-hidden group backdrop-blur-sm hover:bg-white/15 hover:text-white shadow-[0_2px_8px_rgba(0,0,0,0.15)]" >
+ Create new
) : (

No agents available yet.

)}
{[ "Help me brainstorm ideas for a new mobile app", "Generate creative writing prompts for a fantasy novel", "Suggest innovative marketing strategies for a startup", "Create a list of unique product names for a tech company", ].map((prompt, index) => ( ))}
)}
{/* Image preview section */} {flags.IMAGE_UPLOADS_ENABLED && selectedImages.length > 0 && (
{selectedImages.map((image, index) => (
{`Selected
))}
)}
setInput(e.target.value)} onKeyDown={handleKeyDown} placeholder="Write a note, share a hunch, or paste a brief…" disabled={isLoading} rows={1} className="hide-scrollbar w-full resize-none border-0 bg-transparent text-lg text-foreground placeholder:text-muted-foreground/80 focus:outline-none" animate={{ height: textareaHeight }} transition={{ height: { type: "spring", stiffness: 600, damping: 35, mass: 0.5, } }} style={{ overflowY: "auto", minHeight: "32px", maxHeight: "224px", }} />
{isAgentsLoading ? ( Gathering correspondents… ) : agents.length === 0 ? ( No agents configured ) : ( agents.map((entry) => { const isActive = dropdownSelectedId === entry.id return ( handleComposerAgentSelect(entry)} className={`flex w-full items-center justify-between rounded-xl px-3 py-2 text-xs transition ${ isActive ? "bg-white/15 text-white" : "text-white/90 hover:bg-white/5 hover:text-white" }`} > {entry.name} {isActive && Active} ) }) )}
{flags.IMAGE_UPLOADS_ENABLED && ( <> )}
) }