feat(agents): restore agent-worker container architecture + fix chat scroll and parallel chats
- Restore agent-worker from commit 153399f: autonomous HTTP server per agent
(main.go 597 lines, main_test.go 438 lines, Dockerfile.agent-worker)
- Add container fields to agents table (serviceName, servicePort, containerImage, containerStatus)
- Update executor.go: real delegateToAgent() with HTTP POST to agent containers
- Update db.go: GetAgentByID, UpdateContainerStatus, GetAgentHistory, SaveHistory
- Update orchestrator.go: inject DB into executor for container address resolution
- Add tRPC endpoints: agents.deployContainer, agents.stopContainer, agents.containerStatus
- Add Docker Swarm deploy/stop logic in server/agents.ts
- Add Start/Stop container buttons to Agents.tsx with status badges
- Fix chat auto-scroll: replace ScrollArea with overflow-y-auto for direct scrollTop control
- Fix parallel chats: make isThinking per-conversation (thinkingConvId) instead of global
so switching between chats works while one is processing
This commit is contained in:
@@ -93,7 +93,7 @@ function getTs(): string {
|
||||
class ChatStore {
|
||||
private conversations: Conversation[] = loadConversations();
|
||||
private activeId: string = "";
|
||||
private isThinking = false;
|
||||
private thinkingConvId: string | null = null; // per-conversation thinking lock
|
||||
private activeAgents: AgentActivity[] = [];
|
||||
private listeners = new Set<UpdateHandler>();
|
||||
|
||||
@@ -129,7 +129,14 @@ class ChatStore {
|
||||
return this.conversations.find(c => c.id === this.activeId) ?? null;
|
||||
}
|
||||
getIsThinking(): boolean {
|
||||
return this.isThinking;
|
||||
return this.thinkingConvId !== null;
|
||||
}
|
||||
getIsConversationThinking(convId: string): boolean {
|
||||
return this.thinkingConvId === convId;
|
||||
}
|
||||
/** Which conversation is currently thinking, or null */
|
||||
getThinkingConvId(): string | null {
|
||||
return this.thinkingConvId;
|
||||
}
|
||||
|
||||
// ─── Mutations ──────────────────────────────────────────────────────────────
|
||||
@@ -175,8 +182,12 @@ class ChatStore {
|
||||
this.emit();
|
||||
}
|
||||
|
||||
setThinking(v: boolean) {
|
||||
this.isThinking = v;
|
||||
setThinking(v: boolean, convId?: string) {
|
||||
if (v) {
|
||||
this.thinkingConvId = convId ?? this.activeId;
|
||||
} else {
|
||||
this.thinkingConvId = null;
|
||||
}
|
||||
this.emit();
|
||||
}
|
||||
|
||||
@@ -281,7 +292,7 @@ class ChatStore {
|
||||
: c
|
||||
);
|
||||
persistConversations(this.conversations);
|
||||
this.isThinking = false;
|
||||
this.thinkingConvId = null;
|
||||
this.emit();
|
||||
}
|
||||
|
||||
@@ -299,7 +310,7 @@ class ChatStore {
|
||||
c.id === convId ? { ...c, messages: [...c.messages, msg] } : c
|
||||
);
|
||||
persistConversations(this.conversations);
|
||||
this.isThinking = false;
|
||||
this.thinkingConvId = null;
|
||||
this.emit();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ import {
|
||||
Shield,
|
||||
Wrench,
|
||||
Code2,
|
||||
Container,
|
||||
Square,
|
||||
} from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
@@ -74,6 +76,21 @@ function getStatusConfig(status: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function getContainerStatusConfig(status: string | undefined | null) {
|
||||
switch (status) {
|
||||
case "running":
|
||||
return { label: "RUNNING", color: "text-neon-green", bg: "bg-neon-green/15", border: "border-neon-green/30", icon: Container };
|
||||
case "deploying":
|
||||
return { label: "DEPLOYING", color: "text-neon-amber", bg: "bg-neon-amber/15", border: "border-neon-amber/30", icon: Loader2 };
|
||||
case "error":
|
||||
return { label: "ERROR", color: "text-neon-red", bg: "bg-neon-red/15", border: "border-neon-red/30", icon: AlertCircle };
|
||||
case "stopped":
|
||||
default:
|
||||
return { label: "STOPPED", color: "text-muted-foreground", bg: "bg-muted/50", border: "border-border", icon: Square };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function Agents() {
|
||||
const [selectedAgent, setSelectedAgent] = useState<any>(null);
|
||||
const [detailModalOpen, setDetailModalOpen] = useState(false);
|
||||
@@ -97,6 +114,37 @@ export default function Agents() {
|
||||
},
|
||||
});
|
||||
|
||||
const deployMutation = trpc.agents.deployContainer.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data.success) {
|
||||
toast.success(`Agent container deployed: ${data.serviceName}`);
|
||||
} else {
|
||||
toast.error(`Deploy failed: ${data.error}`);
|
||||
}
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Deploy error: ${error.message}`);
|
||||
},
|
||||
});
|
||||
|
||||
const stopMutation = trpc.agents.stopContainer.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data.success) {
|
||||
toast.success("Agent container stopped");
|
||||
} else {
|
||||
toast.error(`Stop failed: ${data.error}`);
|
||||
}
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Stop error: ${error.message}`);
|
||||
},
|
||||
});
|
||||
|
||||
const [deployingAgentId, setDeployingAgentId] = useState<number | null>(null);
|
||||
const [stoppingAgentId, setStoppingAgentId] = useState<number | null>(null);
|
||||
|
||||
const handleDeleteClick = (agentId: number) => {
|
||||
setAgentToDelete(agentId);
|
||||
setDeleteConfirmOpen(true);
|
||||
@@ -139,7 +187,8 @@ 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.filter((a: any) => !a.isOrchestrator).length} agents · {agents.filter((a: any) => a.isOrchestrator).length} orchestrator
|
||||
{agents.filter((a: any) => !a.isOrchestrator).length} agents ·{" "}
|
||||
{agents.filter((a: any) => a.isOrchestrator).length} orchestrator
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -157,7 +206,9 @@ export default function Agents() {
|
||||
<Card className="bg-card border-border/50">
|
||||
<CardContent className="p-12 text-center">
|
||||
<AlertCircle className="w-12 h-12 mx-auto mb-4 text-muted-foreground opacity-50" />
|
||||
<h3 className="text-lg font-semibold text-foreground mb-2">No Agents Deployed</h3>
|
||||
<h3 className="text-lg font-semibold text-foreground mb-2">
|
||||
No Agents Deployed
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
Start by deploying your first AI agent to the cluster.
|
||||
</p>
|
||||
@@ -173,98 +224,144 @@ export default function Agents() {
|
||||
) : (
|
||||
<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 }}>
|
||||
<Card className="bg-card border-cyan-500/40 hover:border-cyan-500/70 transition-all glow-cyan">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<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>
|
||||
<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>
|
||||
{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 }}
|
||||
>
|
||||
<Card className="bg-card border-cyan-500/40 hover:border-cyan-500/70 transition-all glow-cyan">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<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>
|
||||
<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 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>
|
||||
|
||||
<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="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 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>
|
||||
<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 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>
|
||||
|
||||
<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="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 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
|
||||
{agent.allowedTools && agent.allowedTools.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<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-cyan-500/10 text-cyan-400 border border-cyan-500/20"
|
||||
>
|
||||
{tool}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[10px] font-mono text-muted-foreground">Full access</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agent.allowedTools && agent.allowedTools.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<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-cyan-500/10 text-cyan-400 border border-cyan-500/20">
|
||||
{tool}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<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);
|
||||
}}
|
||||
>
|
||||
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`);
|
||||
}}
|
||||
>
|
||||
<BarChart2 className="w-3 h-3 mr-1" /> Metrics
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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); }}
|
||||
>
|
||||
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`); }}
|
||||
>
|
||||
<BarChart2 className="w-3 h-3 mr-1" /> Metrics
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Specialized Agents Section */}
|
||||
{agents.filter((a: any) => !a.isOrchestrator).length > 0 && (
|
||||
@@ -273,126 +370,226 @@ export default function Agents() {
|
||||
<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);
|
||||
{agents
|
||||
.filter((a: any) => !a.isOrchestrator)
|
||||
.map((agent: any, i: number) => {
|
||||
const sc = getStatusConfig(agent.status || "idle");
|
||||
const csc = getContainerStatusConfig(agent.containerStatus);
|
||||
const ContainerIcon = csc.icon;
|
||||
const Icon = ROLE_ICONS[agent.role] || Bot;
|
||||
const temperature =
|
||||
typeof agent.temperature === "string"
|
||||
? parseFloat(agent.temperature)
|
||||
: (agent.temperature ?? 0.7);
|
||||
const isDeploying = deployingAgentId === agent.id;
|
||||
const isStopping = stoppingAgentId === agent.id;
|
||||
|
||||
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>
|
||||
)}
|
||||
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>
|
||||
<p className="text-[11px] text-muted-foreground mt-0.5">{agent.description || "No description"}</p>
|
||||
<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>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<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>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-[9px] font-mono ${csc.bg} ${csc.color} ${csc.border}`}
|
||||
>
|
||||
<ContainerIcon className={`w-2.5 h-2.5 mr-1 ${isDeploying || isStopping ? "animate-spin" : ""}`} />
|
||||
{csc.label}
|
||||
</Badge>
|
||||
</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}
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* 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 && (
|
||||
{/* 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">
|
||||
{agent.containerStatus === "running" ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-[11px] bg-neon-red/15 text-neon-red border border-neon-red/30 hover:bg-neon-red/25"
|
||||
disabled={isStopping}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setStoppingAgentId(agent.id);
|
||||
stopMutation.mutate({ agentId: agent.id }, {
|
||||
onSettled: () => setStoppingAgentId(null),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isStopping ? <Loader2 className="w-3 h-3 mr-1 animate-spin" /> : <Square className="w-3 h-3 mr-1" />}
|
||||
Stop
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-[11px] bg-neon-green/15 text-neon-green border border-neon-green/30 hover:bg-neon-green/25"
|
||||
disabled={isDeploying || agent.containerStatus === "deploying"}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setDeployingAgentId(agent.id);
|
||||
deployMutation.mutate({ agentId: agent.id }, {
|
||||
onSettled: () => setDeployingAgentId(null),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isDeploying || agent.containerStatus === "deploying" ? <Loader2 className="w-3 h-3 mr-1 animate-spin" /> : <Play className="w-3 h-3 mr-1" />}
|
||||
{isDeploying || agent.containerStatus === "deploying" ? "Deploying" : "Deploy"}
|
||||
</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); }}
|
||||
className="h-7 text-[11px] text-primary border-primary/30 hover:bg-primary/10"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleEditAgent(agent);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-3 h-3 mr-1" /> Delete
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
<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>
|
||||
)}
|
||||
@@ -419,7 +616,8 @@ export default function Agents() {
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Agent</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete this agent? This action cannot be undone.
|
||||
Are you sure you want to delete this agent? This action cannot be
|
||||
undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div className="flex gap-3">
|
||||
|
||||
@@ -60,6 +60,9 @@ function useChatStore() {
|
||||
activeId: chatStore.getActiveId(),
|
||||
active: chatStore.getActive(),
|
||||
isThinking: chatStore.getIsThinking(),
|
||||
isThinkingConvId: chatStore.getThinkingConvId(),
|
||||
isConversationThinking: (convId: string) =>
|
||||
chatStore.getIsConversationThinking(convId),
|
||||
activeAgents: chatStore.getActiveAgents(),
|
||||
};
|
||||
}
|
||||
@@ -509,11 +512,13 @@ function ConversationItem({
|
||||
isActive,
|
||||
onClick,
|
||||
onDelete,
|
||||
isThinking,
|
||||
}: {
|
||||
conv: Conversation;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
onDelete: () => void;
|
||||
isThinking?: boolean;
|
||||
}) {
|
||||
const lastMsg = conv.messages[conv.messages.length - 1];
|
||||
const lastContent =
|
||||
@@ -530,12 +535,19 @@ function ConversationItem({
|
||||
className={`group flex items-start gap-1.5 px-2 py-1.5 rounded cursor-pointer transition-colors ${
|
||||
isActive
|
||||
? "bg-[#00D4FF]/10 border border-[#00D4FF]/20"
|
||||
: "hover:bg-secondary/30 border border-transparent"
|
||||
: isThinking
|
||||
? "bg-[#FFB800]/5 border border-[#FFB800]/20"
|
||||
: "hover:bg-secondary/30 border border-transparent"
|
||||
}`}
|
||||
>
|
||||
<MessageSquare
|
||||
className={`w-3 h-3 mt-0.5 shrink-0 ${isActive ? "text-[#00D4FF]" : "text-muted-foreground/40"}`}
|
||||
/>
|
||||
<div className="relative">
|
||||
<MessageSquare
|
||||
className={`w-3 h-3 mt-0.5 shrink-0 ${isActive ? "text-[#00D4FF]" : isThinking ? "text-[#FFB800]" : "text-muted-foreground/40"}`}
|
||||
/>
|
||||
{isThinking && (
|
||||
<span className="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-[#FFB800] animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p
|
||||
className={`text-[10px] font-mono truncate ${isActive ? "text-foreground" : "text-foreground/70"}`}
|
||||
@@ -575,8 +587,15 @@ function ConversationItem({
|
||||
// ─── Main Chat Component ──────────────────────────────────────────────────────
|
||||
|
||||
export default function Chat() {
|
||||
const { conversations, activeId, active, isThinking, activeAgents } =
|
||||
useChatStore();
|
||||
const {
|
||||
conversations,
|
||||
activeId,
|
||||
active,
|
||||
isThinking,
|
||||
isThinkingConvId,
|
||||
isConversationThinking,
|
||||
activeAgents,
|
||||
} = useChatStore();
|
||||
const [input, setInput] = useState("");
|
||||
const [activeTab, setActiveTab] = useState<SidebarTab>("console");
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
@@ -598,11 +617,21 @@ export default function Chat() {
|
||||
}
|
||||
}, [orchestratorConfigQuery.data, conversations.length]);
|
||||
|
||||
// Auto-scroll
|
||||
// Auto-scroll to bottom when messages change
|
||||
useEffect(() => {
|
||||
if (scrollRef.current)
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}, [active?.messages]);
|
||||
const el = scrollRef.current;
|
||||
if (!el) return;
|
||||
const scrollToBottom = () => {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
};
|
||||
requestAnimationFrame(scrollToBottom);
|
||||
const t1 = setTimeout(scrollToBottom, 50);
|
||||
const t2 = setTimeout(scrollToBottom, 200);
|
||||
return () => {
|
||||
clearTimeout(t1);
|
||||
clearTimeout(t2);
|
||||
};
|
||||
}, [active?.messages, isCurrentConvThinking, activeAgents]);
|
||||
|
||||
// Auto-resize textarea
|
||||
const adjustTextareaHeight = useCallback(() => {
|
||||
@@ -718,15 +747,19 @@ export default function Chat() {
|
||||
activeId,
|
||||
]);
|
||||
|
||||
const isCurrentConvThinking = activeId
|
||||
? isConversationThinking(activeId)
|
||||
: false;
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!input.trim() || isThinking) return;
|
||||
if (!input.trim() || isCurrentConvThinking) return;
|
||||
|
||||
const { convId: cid, newHistory } = chatStore.addUserMessage(
|
||||
input.trim(),
|
||||
convId
|
||||
);
|
||||
setInput("");
|
||||
chatStore.setThinking(true);
|
||||
chatStore.setThinking(true, cid);
|
||||
setActiveTab("console");
|
||||
|
||||
const thinkingId = chatStore.addThinkingMessage(cid);
|
||||
@@ -877,6 +910,7 @@ export default function Chat() {
|
||||
key={c.id}
|
||||
conv={c}
|
||||
isActive={c.id === activeId}
|
||||
isThinking={isConversationThinking(c.id)}
|
||||
onClick={() => chatStore.setActiveId(c.id)}
|
||||
onDelete={() => chatStore.deleteConversation(c.id)}
|
||||
/>
|
||||
@@ -888,14 +922,14 @@ export default function Chat() {
|
||||
{/* ─── Center — Chat Area ─── */}
|
||||
<Card className="flex-1 bg-card border-border/50 overflow-hidden min-w-0">
|
||||
<CardContent className="p-0 h-full flex flex-col">
|
||||
<ScrollArea className="flex-1">
|
||||
<div ref={scrollRef} className="p-4 space-y-4">
|
||||
<div ref={scrollRef} className="flex-1 overflow-y-auto">
|
||||
<div className="p-4 space-y-4">
|
||||
<AnimatePresence initial={false}>
|
||||
{messages.map(msg => (
|
||||
<MessageBubble key={msg.id} msg={msg} />
|
||||
))}
|
||||
</AnimatePresence>
|
||||
{isThinking && (
|
||||
{isCurrentConvThinking && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
@@ -914,7 +948,7 @@ export default function Chat() {
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* Input area */}
|
||||
<div className="border-t border-border/50 p-3 bg-secondary/10 shrink-0">
|
||||
@@ -945,21 +979,21 @@ export default function Chat() {
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
isThinking
|
||||
isCurrentConvThinking
|
||||
? "Ожидание ответа..."
|
||||
: "Введите команду... (Shift+Enter для новой строки)"
|
||||
}
|
||||
disabled={isThinking}
|
||||
disabled={isCurrentConvThinking}
|
||||
rows={1}
|
||||
className="flex-1 bg-transparent text-foreground font-mono text-sm placeholder:text-muted-foreground/50 focus:outline-none resize-none min-h-[32px] max-h-[150px] py-1.5"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={sendMessage}
|
||||
disabled={isThinking || !input.trim()}
|
||||
disabled={isCurrentConvThinking || !input.trim()}
|
||||
className="bg-[#00D4FF]/15 text-[#00D4FF] border border-[#00D4FF]/30 hover:bg-[#00D4FF]/25 h-8 w-8 p-0 shrink-0"
|
||||
>
|
||||
{isThinking ? (
|
||||
{isCurrentConvThinking ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
|
||||
Reference in New Issue
Block a user