| // src/components/async/MultiAgentDebatePanel.tsx | |
| import { useState, useEffect, useCallback, useRef } from "react"; | |
| import api from "@/lib/api"; | |
| import { useAuth } from "@/context/AuthContext"; | |
| import { toast } from "react-toastify"; | |
| import { | |
| Brain, Send, Users, ChevronDown, ChevronUp, Sparkles, | |
| ThumbsUp, ThumbsDown, HelpCircle, MessageCircle, GitBranch, | |
| Eye, Loader2, Bot, User, Shield, X, | |
| } from "lucide-react"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| interface AgentPersona { | |
| id: string; | |
| name: string; | |
| role: string; | |
| expertise: string[]; | |
| background: string; | |
| methodology: string; | |
| avatarColor: string; | |
| } | |
| interface DebateMessage { | |
| id: string; | |
| agentId: string; | |
| agentName: string; | |
| content: string; | |
| stance: "neutral" | "agree" | "disagree" | "question"; | |
| references: string[]; | |
| timestamp: string; | |
| isUserMessage?: boolean; | |
| } | |
| interface MultiAgentDebatePanelProps { | |
| topic: string; | |
| onClose?: () => void; | |
| } | |
| const MOCK_AGENTS: AgentPersona[] = [ | |
| { | |
| id: "agent-methodologist", name: "Dr. Chen", role: "Methodologist", | |
| expertise: ["statistics", "experimental design", "causal inference"], | |
| background: "Professor of Biostatistics with 20 years of experience in clinical trial design", | |
| methodology: "Frequentist and Bayesian statistical methods, meta-analysis", | |
| avatarColor: "#0d9488", | |
| }, | |
| { | |
| id: "agent-domain", name: "Dr. Patel", role: "Domain Expert", | |
| expertise: ["molecular biology", "genomics", "CRISPR"], | |
| background: "Leading researcher in gene editing technologies, PI at Broad Institute", | |
| methodology: "Experimental validation, in-vitro and in-vivo models", | |
| avatarColor: "#7c3aed", | |
| }, | |
| { | |
| id: "agent-skeptic", name: "Dr. Rivera", role: "Critical Reviewer", | |
| expertise: ["research methodology", "reproducibility", "meta-science"], | |
| background: "Meta-researcher focused on improving scientific rigor and reproducibility", | |
| methodology: "Systematic review, replication studies, bias detection", | |
| avatarColor: "#db2777", | |
| }, | |
| { | |
| id: "agent-innovator", name: "Dr. Kim", role: "Innovation Scout", | |
| expertise: ["emerging technologies", "cross-disciplinary research", "AI/ML"], | |
| background: "Interdisciplinary researcher bridging computational and experimental science", | |
| methodology: "Computational modeling, ML-driven hypothesis generation", | |
| avatarColor: "#ea580c", | |
| }, | |
| ]; | |
| export function MultiAgentDebatePanel({ topic, onClose }: MultiAgentDebatePanelProps) { | |
| const { user } = useAuth(); | |
| const [agents] = useState<AgentPersona[]>(MOCK_AGENTS); | |
| const [messages, setMessages] = useState<DebateMessage[]>([ | |
| { | |
| id: "intro", agentId: "system", agentName: "System", | |
| content: `Multi-agent debate initiated on: "${topic}". Each agent will contribute from their expertise. You can reply, ask questions, or steer the discussion.`, | |
| stance: "neutral", references: [], timestamp: new Date().toISOString(), | |
| }, | |
| ]); | |
| const [isProcessing, setIsProcessing] = useState(false); | |
| const [userInput, setUserInput] = useState(""); | |
| const [selectedStance, setSelectedStance] = useState<"agree" | "disagree" | "question" | null>(null); | |
| const [showAgentProfile, setShowAgentProfile] = useState<string | null>(null); | |
| const messagesEndRef = useRef<HTMLDivElement>(null); | |
| const addAgentMessage = useCallback((agentId: string, content: string, stance: DebateMessage["stance"], references: string[] = []) => { | |
| const agent = agents.find(a => a.id === agentId); | |
| const msg: DebateMessage = { | |
| id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, | |
| agentId, agentName: agent?.name ?? "Agent", content, stance, references, | |
| timestamp: new Date().toISOString(), | |
| }; | |
| setMessages(prev => [...prev, msg]); | |
| }, [agents]); | |
| const triggerAgentResponse = useCallback(async (agentId: string) => { | |
| try { | |
| const res = await api.post("/ai/multi-agent/respond", { | |
| topic, agentId, history: messages.slice(-10), | |
| }); | |
| const data = res.data; | |
| addAgentMessage(agentId, data.content, data.stance ?? "neutral", data.references ?? []); | |
| } catch (error) { | |
| console.error("Agent response error:", error); | |
| addAgentMessage(agentId, "I need more context to provide a meaningful response on this topic.", "neutral"); | |
| } | |
| }, [topic, messages, addAgentMessage]); | |
| const handleUserReply = async () => { | |
| if (!userInput.trim() || isProcessing) return; | |
| const userMsg: DebateMessage = { | |
| id: `user-${Date.now()}`, | |
| agentId: "user", agentName: user?.username ?? "You", | |
| content: userInput.trim(), stance: selectedStance ?? "neutral", | |
| references: [], timestamp: new Date().toISOString(), isUserMessage: true, | |
| }; | |
| setMessages(prev => [...prev, userMsg]); | |
| setUserInput(""); | |
| setSelectedStance(null); | |
| setIsProcessing(true); | |
| try { | |
| // Trigger all agents in parallel for better performance | |
| await Promise.all(agents.map(agent => triggerAgentResponse(agent.id))); | |
| } catch (error) { | |
| toast.error("Some agents failed to respond"); | |
| } finally { | |
| setIsProcessing(false); | |
| } | |
| }; | |
| const toggleAgentProfile = (agentId: string) => { | |
| setShowAgentProfile(prev => prev === agentId ? null : agentId); | |
| }; | |
| useEffect(() => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
| }, [messages]); | |
| return ( | |
| <div className="glass rounded-2xl border border-white/20 dark:border-white/10 overflow-hidden flex flex-col" style={{ maxHeight: "80vh" }}> | |
| {/* Header */} | |
| <div className="px-5 py-4 border-b border-white/10 flex items-center justify-between bg-linear-to-r from-purple-500/5 to-blue-500/5"> | |
| <div className="flex items-center gap-3"> | |
| <Brain className="w-5 h-5 text-purple-500" /> | |
| <div> | |
| <h3 className="font-bold text-sm">Multi‑Agent Panel Discussion</h3> | |
| <p className="text-[10px] text-gray-500 truncate max-w-md">{topic}</p> | |
| </div> | |
| </div> | |
| {onClose && ( | |
| <button onClick={onClose} className="p-1 text-gray-400 hover:text-gray-600 rounded-full"> | |
| <X className="w-4 h-4" /> | |
| </button> | |
| )} | |
| </div> | |
| {/* Agent roster */} | |
| <div className="px-4 py-2 border-b border-white/10 flex gap-2 overflow-x-auto"> | |
| {agents.map(agent => ( | |
| <button | |
| key={agent.id} | |
| onClick={() => toggleAgentProfile(agent.id)} | |
| className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium transition whitespace-nowrap ${ | |
| showAgentProfile === agent.id | |
| ? "bg-purple-500/20 text-purple-600 dark:text-purple-400" | |
| : "bg-white/20 dark:bg-white/10 text-gray-600 dark:text-gray-400 hover:bg-white/30" | |
| }`} | |
| > | |
| <div | |
| className="w-5 h-5 rounded-full flex items-center justify-center text-white text-[10px] font-bold" | |
| style={{ backgroundColor: agent.avatarColor }} | |
| > | |
| {agent.name.split(" ")[1]?.[0] ?? agent.name[0]} | |
| </div> | |
| {agent.name} | |
| </button> | |
| ))} | |
| </div> | |
| {/* Agent profile (collapsible) */} | |
| <AnimatePresence> | |
| {showAgentProfile && (() => { | |
| const agent = agents.find(a => a.id === showAgentProfile); | |
| if (!agent) return null; | |
| return ( | |
| <motion.div | |
| initial={{ height: 0, opacity: 0 }} | |
| animate={{ height: "auto", opacity: 1 }} | |
| exit={{ height: 0, opacity: 0 }} | |
| className="overflow-hidden border-b border-white/10" | |
| > | |
| <div className="px-5 py-3 bg-white/10 dark:bg-white/5"> | |
| <div className="flex items-center gap-3 mb-2"> | |
| <div | |
| className="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold" | |
| style={{ backgroundColor: agent.avatarColor }} | |
| > | |
| {agent.name.split(" ")[0]?.[0]}{agent.name.split(" ")[1]?.[0]} | |
| </div> | |
| <div> | |
| <p className="font-semibold text-sm">{agent.name}</p> | |
| <p className="text-xs text-gray-500">{agent.role}</p> | |
| </div> | |
| </div> | |
| <p className="text-xs text-gray-600 dark:text-gray-400 mb-2">{agent.background}</p> | |
| <div className="flex flex-wrap gap-1"> | |
| {agent.expertise.map(exp => ( | |
| <span | |
| key={exp} | |
| className="text-[9px] bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 px-2 py-0.5 rounded-full" | |
| > | |
| {exp} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| </motion.div> | |
| ); | |
| })()} | |
| </AnimatePresence> | |
| {/* Messages list */} | |
| <div className="flex-1 overflow-y-auto px-4 py-3 space-y-3"> | |
| {messages.map(msg => { | |
| const agent = agents.find(a => a.id === msg.agentId); | |
| const isUser = msg.isUserMessage; | |
| return ( | |
| <div key={msg.id} className={`flex ${isUser ? "justify-end" : "justify-start"}`}> | |
| <div className={`max-w-[85%] ${isUser ? "" : "flex gap-3"}`}> | |
| {!isUser && ( | |
| <div | |
| className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold shrink-0 mt-1" | |
| style={{ backgroundColor: agent?.avatarColor ?? "#6b7280" }} | |
| > | |
| {msg.agentName[0]} | |
| </div> | |
| )} | |
| <div | |
| className={`rounded-2xl px-4 py-2.5 text-sm ${ | |
| isUser | |
| ? "bg-primary text-white rounded-br-sm" | |
| : "bg-white/30 dark:bg-white/10 text-gray-800 dark:text-gray-200 rounded-bl-sm" | |
| }`} | |
| > | |
| {!isUser && <p className="text-[10px] font-bold mb-0.5 opacity-70">{msg.agentName}</p>} | |
| <p className="whitespace-pre-wrap">{msg.content}</p> | |
| {/* Stance badge */} | |
| {msg.stance !== "neutral" && ( | |
| <span | |
| className={`inline-flex items-center gap-1 text-[9px] px-2 py-0.5 rounded-full mt-1.5 ${ | |
| msg.stance === "agree" | |
| ? "bg-green-100 dark:bg-green-900/30 text-green-700" | |
| : msg.stance === "disagree" | |
| ? "bg-red-100 dark:bg-red-900/30 text-red-700" | |
| : "bg-amber-100 dark:bg-amber-900/30 text-amber-700" | |
| }`} | |
| > | |
| {msg.stance === "agree" ? <ThumbsUp className="w-3 h-3" /> : msg.stance === "disagree" ? <ThumbsDown className="w-3 h-3" /> : <HelpCircle className="w-3 h-3" />} | |
| {msg.stance} | |
| </span> | |
| )} | |
| {/* References */} | |
| {msg.references.length > 0 && ( | |
| <div className="mt-2 pt-2 border-t border-white/10"> | |
| <p className="text-[9px] text-gray-400 mb-1">References:</p> | |
| {msg.references.map((ref, i) => ( | |
| <span key={i} className="text-[9px] bg-white/20 dark:bg-white/10 px-2 py-0.5 rounded-full mr-1"> | |
| {ref} | |
| </span> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| {/* Input area */} | |
| <div className="px-4 py-3 border-t border-white/10"> | |
| {/* Stance selector */} | |
| <div className="flex gap-1 mb-2"> | |
| {[ | |
| { stance: "agree" as const, icon: ThumbsUp, label: "Agree", color: "text-green-600" }, | |
| { stance: "disagree" as const, icon: ThumbsDown, label: "Disagree", color: "text-red-600" }, | |
| { stance: "question" as const, icon: HelpCircle, label: "Question", color: "text-amber-600" }, | |
| ].map(s => ( | |
| <button | |
| key={s.stance} | |
| onClick={() => setSelectedStance(prev => (prev === s.stance ? null : s.stance))} | |
| className={`flex items-center gap-1 px-3 py-1 rounded-full text-xs transition ${ | |
| selectedStance === s.stance | |
| ? "bg-white/30 dark:bg-white/20 font-semibold" | |
| : "bg-white/10 hover:bg-white/20" | |
| }`} | |
| > | |
| <s.icon className={`w-3.5 h-3.5 ${s.color}`} /> {s.label} | |
| </button> | |
| ))} | |
| </div> | |
| <div className="flex gap-2"> | |
| <input | |
| type="text" | |
| value={userInput} | |
| onChange={e => setUserInput(e.target.value)} | |
| onKeyDown={e => e.key === "Enter" && handleUserReply()} | |
| placeholder="Steer the discussion or ask a question..." | |
| className="flex-1 px-4 py-2 bg-white/20 dark:bg-white/10 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary" | |
| disabled={isProcessing} | |
| /> | |
| <button | |
| onClick={handleUserReply} | |
| disabled={!userInput.trim() || isProcessing} | |
| className="p-2.5 bg-primary text-white rounded-xl hover:bg-primary/80 disabled:opacity-50 transition" | |
| > | |
| {isProcessing ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 13.8 kB
- Xet hash:
- a2d90afb24947198fd1d04cd16562da17b8991639495ac8edc095d88c8967ac8
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.