tahamajs/IE / Projects /front /src /components /async /MultiAgentDebatePanel.tsx
tahamajs's picture
download
raw
13.8 kB
// 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.