Checkpoint: Phase 7 complete: Orchestrator Agent добавлен в /agents с меткой CROWN/SYSTEM, кнопками Configure и Open Chat. /chat читает конфиг оркестратора из БД (модель, промпт, инструменты). AgentDetailModal поддерживает isOrchestrator. 24 теста пройдены.
This commit is contained in:
@@ -36,6 +36,11 @@ import {
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
BarChart2,
|
||||
Crown,
|
||||
MessageSquare,
|
||||
Shield,
|
||||
Wrench,
|
||||
Code2,
|
||||
} from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
@@ -50,6 +55,10 @@ const ROLE_ICONS: Record<string, any> = {
|
||||
researcher: Brain,
|
||||
executor: Zap,
|
||||
monitor: Eye,
|
||||
orchestrator: Crown,
|
||||
browser: Globe,
|
||||
tool_builder: Wrench,
|
||||
agent_compiler: Code2,
|
||||
};
|
||||
|
||||
function getStatusConfig(status: string) {
|
||||
@@ -130,7 +139,7 @@ export default function Agents() {
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground">Agent Fleet</h2>
|
||||
<p className="text-sm text-muted-foreground font-mono mt-1">
|
||||
{agents.length} total agents
|
||||
{agents.filter((a: any) => !a.isOrchestrator).length} agents · {agents.filter((a: any) => a.isOrchestrator).length} orchestrator
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -162,83 +171,63 @@ export default function Agents() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
/* Agent cards grid */
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{agents.map((agent: any, i: number) => {
|
||||
const sc = getStatusConfig(agent.status || "idle");
|
||||
const Icon = ROLE_ICONS[agent.role] || Bot;
|
||||
const temperature = typeof agent.temperature === "string" ? parseFloat(agent.temperature) : agent.temperature;
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Orchestrator Section */}
|
||||
{agents.filter((a: any) => a.isOrchestrator).map((agent: any) => {
|
||||
const temperature = typeof agent.temperature === "string" ? parseFloat(agent.temperature) : (agent.temperature ?? 0.7);
|
||||
return (
|
||||
<motion.div
|
||||
key={agent.id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.08 }}
|
||||
>
|
||||
<Card className={`bg-card border-border/50 hover:border-primary/30 transition-all cursor-pointer ${sc.glow}`} onClick={() => handleEditAgent(agent)}>
|
||||
<motion.div key={agent.id} initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}>
|
||||
<Card className="bg-card border-cyan-500/40 hover:border-cyan-500/70 transition-all glow-cyan">
|
||||
<CardContent className="p-5">
|
||||
{/* Top row */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-secondary/50 border border-border/50 flex items-center justify-center">
|
||||
<Icon className={`w-5 h-5 ${sc.color}`} />
|
||||
<div className="w-12 h-12 rounded-lg bg-cyan-500/15 border border-cyan-500/40 flex items-center justify-center">
|
||||
<Crown className="w-6 h-6 text-cyan-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-foreground">{agent.name}</h3>
|
||||
<p className="text-[11px] text-muted-foreground mt-0.5">{agent.description || "No description"}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-base font-bold text-foreground">{agent.name}</h3>
|
||||
<Badge className="text-[9px] font-mono bg-cyan-500/20 text-cyan-300 border-cyan-500/40 px-1.5 py-0">
|
||||
<Crown className="w-2.5 h-2.5 mr-1" /> ORCHESTRATOR
|
||||
</Badge>
|
||||
<Badge className="text-[9px] font-mono bg-amber-500/20 text-amber-300 border-amber-500/40 px-1.5 py-0">
|
||||
<Shield className="w-2.5 h-2.5 mr-1" /> SYSTEM
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground mt-0.5">{agent.description || "Main orchestrator — controls all agents, tools and system resources"}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className={`text-[10px] font-mono ${sc.badge}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${sc.bg} mr-1.5 ${agent.isActive ? "pulse-indicator" : ""}`} />
|
||||
{agent.isActive ? "ACTIVE" : "INACTIVE"}
|
||||
<Badge variant="outline" className="text-[10px] font-mono bg-neon-green/15 text-neon-green border-neon-green/30">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-neon-green mr-1.5 pulse-indicator" />
|
||||
ACTIVE
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Model & Node info */}
|
||||
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||
<div className="p-2.5 rounded-md bg-secondary/30 border border-border/20">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<Brain className="w-3 h-3 text-primary" />
|
||||
<span className="text-[10px] text-muted-foreground font-mono">MODEL</span>
|
||||
</div>
|
||||
<div className="text-xs font-mono font-medium text-primary">{agent.model}</div>
|
||||
<div className="grid grid-cols-3 gap-3 mb-4">
|
||||
<div className="p-2.5 rounded-md bg-cyan-500/10 border border-cyan-500/20">
|
||||
<div className="text-[10px] text-muted-foreground font-mono mb-1">MODEL</div>
|
||||
<div className="text-xs font-mono font-bold text-cyan-400">{agent.model}</div>
|
||||
<div className="text-[10px] font-mono text-muted-foreground">{agent.provider}</div>
|
||||
</div>
|
||||
<div className="p-2.5 rounded-md bg-secondary/30 border border-border/20">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<Cpu className="w-3 h-3 text-primary" />
|
||||
<span className="text-[10px] text-muted-foreground font-mono">CONFIG</span>
|
||||
</div>
|
||||
<div className="text-xs font-mono font-medium text-foreground">T: {temperature.toFixed(2)}</div>
|
||||
<div className="text-[10px] text-muted-foreground font-mono mb-1">TEMPERATURE</div>
|
||||
<div className="text-xs font-mono font-bold text-foreground">{temperature.toFixed(2)}</div>
|
||||
<div className="text-[10px] font-mono text-muted-foreground">Tokens: {agent.maxTokens}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics row */}
|
||||
<div className="flex items-center gap-4 mb-4 text-[10px] font-mono">
|
||||
<div className="flex items-center gap-1">
|
||||
<Zap className="w-3 h-3 text-neon-amber" />
|
||||
<span className="text-muted-foreground">Role:</span>
|
||||
<span className="text-foreground font-medium capitalize">{agent.role}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3 text-primary" />
|
||||
<span className="text-muted-foreground">Created:</span>
|
||||
<span className="text-foreground font-medium">{new Date(agent.createdAt).toLocaleDateString()}</span>
|
||||
<div className="p-2.5 rounded-md bg-secondary/30 border border-border/20">
|
||||
<div className="text-[10px] text-muted-foreground font-mono mb-1">TOOLS</div>
|
||||
<div className="text-xs font-mono font-bold text-foreground">
|
||||
{agent.allowedTools?.length ?? 0} tools
|
||||
</div>
|
||||
<div className="text-[10px] font-mono text-muted-foreground">Full access</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tools */}
|
||||
{agent.allowedTools && agent.allowedTools.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<span className="text-[10px] text-muted-foreground font-mono block mb-1.5">TOOLS</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{agent.allowedTools.map((tool: string) => (
|
||||
<span
|
||||
key={tool}
|
||||
className="px-2 py-0.5 rounded text-[10px] font-mono bg-primary/10 text-primary border border-primary/20"
|
||||
>
|
||||
<span key={tool} className="px-2 py-0.5 rounded text-[10px] font-mono bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
|
||||
{tool}
|
||||
</span>
|
||||
))}
|
||||
@@ -246,47 +235,167 @@ export default function Agents() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2 pt-3 border-t border-border/30">
|
||||
<div className="flex items-center gap-2 pt-3 border-t border-cyan-500/20">
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-[11px] bg-cyan-500/15 text-cyan-400 border border-cyan-500/30 hover:bg-cyan-500/25"
|
||||
onClick={(e) => { e.stopPropagation(); navigate("/chat"); }}
|
||||
>
|
||||
<MessageSquare className="w-3 h-3 mr-1" /> Open Chat
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 text-[11px] text-primary border-primary/30 hover:bg-primary/10"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditAgent(agent);
|
||||
}}
|
||||
onClick={(e) => { e.stopPropagation(); handleEditAgent(agent); }}
|
||||
>
|
||||
Edit
|
||||
Configure
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 text-[11px] text-neon-amber border-neon-amber/30 hover:bg-neon-amber/10"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/agents/${agent.id}/metrics`);
|
||||
}}
|
||||
onClick={(e) => { e.stopPropagation(); navigate(`/agents/${agent.id}/metrics`); }}
|
||||
>
|
||||
<BarChart2 className="w-3 h-3 mr-1" /> Metrics
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 text-[11px] text-neon-red border-neon-red/30 hover:bg-neon-red/10 ml-auto"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteClick(agent.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-3 h-3 mr-1" /> Delete
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Specialized Agents Section */}
|
||||
{agents.filter((a: any) => !a.isOrchestrator).length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-muted-foreground font-mono mb-3 flex items-center gap-2">
|
||||
<Bot className="w-4 h-4" /> SPECIALIZED AGENTS
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{agents.filter((a: any) => !a.isOrchestrator).map((agent: any, i: number) => {
|
||||
const sc = getStatusConfig(agent.status || "idle");
|
||||
const Icon = ROLE_ICONS[agent.role] || Bot;
|
||||
const temperature = typeof agent.temperature === "string" ? parseFloat(agent.temperature) : (agent.temperature ?? 0.7);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={agent.id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.08 }}
|
||||
>
|
||||
<Card className={`bg-card border-border/50 hover:border-primary/30 transition-all cursor-pointer ${sc.glow}`} onClick={() => handleEditAgent(agent)}>
|
||||
<CardContent className="p-5">
|
||||
{/* Top row */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-secondary/50 border border-border/50 flex items-center justify-center">
|
||||
<Icon className={`w-5 h-5 ${sc.color}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<h3 className="text-sm font-semibold text-foreground">{agent.name}</h3>
|
||||
{agent.isSystem && (
|
||||
<Badge className="text-[9px] font-mono bg-amber-500/15 text-amber-400 border-amber-500/30 px-1 py-0">
|
||||
SYS
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground mt-0.5">{agent.description || "No description"}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className={`text-[10px] font-mono ${sc.badge}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${sc.bg} mr-1.5 ${agent.isActive ? "pulse-indicator" : ""}`} />
|
||||
{agent.isActive ? "ACTIVE" : "INACTIVE"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Model & Config */}
|
||||
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||
<div className="p-2.5 rounded-md bg-secondary/30 border border-border/20">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<Brain className="w-3 h-3 text-primary" />
|
||||
<span className="text-[10px] text-muted-foreground font-mono">MODEL</span>
|
||||
</div>
|
||||
<div className="text-xs font-mono font-medium text-primary">{agent.model}</div>
|
||||
<div className="text-[10px] font-mono text-muted-foreground">{agent.provider}</div>
|
||||
</div>
|
||||
<div className="p-2.5 rounded-md bg-secondary/30 border border-border/20">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<Cpu className="w-3 h-3 text-primary" />
|
||||
<span className="text-[10px] text-muted-foreground font-mono">CONFIG</span>
|
||||
</div>
|
||||
<div className="text-xs font-mono font-medium text-foreground">T: {temperature.toFixed(2)}</div>
|
||||
<div className="text-[10px] font-mono text-muted-foreground">Tokens: {agent.maxTokens}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Role & Date */}
|
||||
<div className="flex items-center gap-4 mb-4 text-[10px] font-mono">
|
||||
<div className="flex items-center gap-1">
|
||||
<Zap className="w-3 h-3 text-neon-amber" />
|
||||
<span className="text-muted-foreground">Role:</span>
|
||||
<span className="text-foreground font-medium capitalize">{agent.role}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3 text-primary" />
|
||||
<span className="text-muted-foreground">Created:</span>
|
||||
<span className="text-foreground font-medium">{new Date(agent.createdAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tools */}
|
||||
{agent.allowedTools && agent.allowedTools.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<span className="text-[10px] text-muted-foreground font-mono block mb-1.5">TOOLS</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{agent.allowedTools.map((tool: string) => (
|
||||
<span key={tool} className="px-2 py-0.5 rounded text-[10px] font-mono bg-primary/10 text-primary border border-primary/20">
|
||||
{tool}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2 pt-3 border-t border-border/30">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 text-[11px] text-primary border-primary/30 hover:bg-primary/10"
|
||||
onClick={(e) => { e.stopPropagation(); handleEditAgent(agent); }}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 text-[11px] text-neon-amber border-neon-amber/30 hover:bg-neon-amber/10"
|
||||
onClick={(e) => { e.stopPropagation(); navigate(`/agents/${agent.id}/metrics`); }}
|
||||
>
|
||||
<BarChart2 className="w-3 h-3 mr-1" /> Metrics
|
||||
</Button>
|
||||
{!agent.isSystem && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 text-[11px] text-neon-red border-neon-red/30 hover:bg-neon-red/10 ml-auto"
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteClick(agent.id); }}
|
||||
>
|
||||
<Trash2 className="w-3 h-3 mr-1" /> Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -237,15 +237,7 @@ function MessageBubble({ msg }: { msg: ChatMessage }) {
|
||||
// ─── Main Chat Component ──────────────────────────────────────────────────────
|
||||
|
||||
export default function Chat() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{
|
||||
id: "welcome",
|
||||
role: "system",
|
||||
content:
|
||||
"GoClaw Orchestrator ready.\nI have access to all agents, tools, and skills.\nType a command or ask anything.",
|
||||
timestamp: new Date().toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" }),
|
||||
},
|
||||
]);
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [conversationHistory, setConversationHistory] = useState<
|
||||
Array<{ role: "user" | "assistant" | "system"; content: string }>
|
||||
>([]);
|
||||
@@ -256,6 +248,22 @@ export default function Chat() {
|
||||
|
||||
const agentsQuery = trpc.agents.list.useQuery(undefined, { refetchInterval: 30000 });
|
||||
const orchestratorMutation = trpc.orchestrator.chat.useMutation();
|
||||
const orchestratorConfigQuery = trpc.orchestrator.getConfig.useQuery();
|
||||
|
||||
// Initialize welcome message with orchestrator name from DB
|
||||
useEffect(() => {
|
||||
if (orchestratorConfigQuery.data && messages.length === 0) {
|
||||
const cfg = orchestratorConfigQuery.data;
|
||||
setMessages([
|
||||
{
|
||||
id: "welcome",
|
||||
role: "system",
|
||||
content: `${cfg.name} ready. Model: ${cfg.model}\nI have access to all agents, tools, and skills.\nType a command or ask anything.`,
|
||||
timestamp: new Date().toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" }),
|
||||
},
|
||||
]);
|
||||
}
|
||||
}, [orchestratorConfigQuery.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
@@ -364,7 +372,8 @@ export default function Chat() {
|
||||
};
|
||||
|
||||
const agents = agentsQuery.data ?? [];
|
||||
const activeAgents = agents.filter((a) => a.isActive);
|
||||
const activeAgents = agents.filter((a) => a.isActive && !(a as any).isOrchestrator);
|
||||
const orchConfig = orchestratorConfigQuery.data;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-3">
|
||||
@@ -375,27 +384,44 @@ export default function Chat() {
|
||||
<Bot className="w-4 h-4 text-cyan-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-foreground">GoClaw Orchestrator</h2>
|
||||
<h2 className="text-lg font-bold text-foreground">
|
||||
{orchConfig?.name ?? "GoClaw Orchestrator"}
|
||||
</h2>
|
||||
<p className="text-[11px] font-mono text-muted-foreground">
|
||||
Main AI · {activeAgents.length} agents · {ORCHESTRATOR_TOOLS_COUNT} tools
|
||||
{orchConfig ? (
|
||||
<span>
|
||||
<span className="text-cyan-400/80">{orchConfig.model}</span>
|
||||
{" · "}{activeAgents.length} agents · {ORCHESTRATOR_TOOLS_COUNT} tools
|
||||
</span>
|
||||
) : (
|
||||
`Main AI · ${activeAgents.length} agents · ${ORCHESTRATOR_TOOLS_COUNT} tools`
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active agents badges */}
|
||||
<div className="flex items-center gap-1.5 flex-wrap justify-end">
|
||||
{activeAgents.slice(0, 4).map((agent) => (
|
||||
<Badge
|
||||
key={agent.id}
|
||||
variant="outline"
|
||||
className="text-[9px] h-5 px-1.5 font-mono border-border/50 text-muted-foreground"
|
||||
>
|
||||
{agent.role === "browser" && <Globe className="w-2.5 h-2.5 mr-1 text-cyan-400" />}
|
||||
{agent.role === "tool_builder" && <Wrench className="w-2.5 h-2.5 mr-1 text-orange-400" />}
|
||||
{agent.role === "agent_compiler" && <Cpu className="w-2.5 h-2.5 mr-1 text-purple-400" />}
|
||||
{agent.name}
|
||||
</Badge>
|
||||
))}
|
||||
{/* Active agents badges + Configure link */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5 flex-wrap justify-end">
|
||||
{activeAgents.slice(0, 3).map((agent) => (
|
||||
<Badge
|
||||
key={agent.id}
|
||||
variant="outline"
|
||||
className="text-[9px] h-5 px-1.5 font-mono border-border/50 text-muted-foreground"
|
||||
>
|
||||
{agent.role === "browser" && <Globe className="w-2.5 h-2.5 mr-1 text-cyan-400" />}
|
||||
{agent.role === "tool_builder" && <Wrench className="w-2.5 h-2.5 mr-1 text-orange-400" />}
|
||||
{agent.role === "agent_compiler" && <Cpu className="w-2.5 h-2.5 mr-1 text-purple-400" />}
|
||||
{agent.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<a
|
||||
href="/agents"
|
||||
className="text-[10px] font-mono px-2 py-1 rounded border border-cyan-500/30 text-cyan-400/70 hover:text-cyan-400 hover:border-cyan-500/60 transition-colors bg-cyan-500/5 shrink-0"
|
||||
>
|
||||
Configure
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user