feat(chat): Mission Control UI — tabbed sidebar with Console/Tasks/Research, dark theme, resizable textarea
- TasksPanel: rewritten with Mission Control dark theme (cyan/green/amber/red neon accents, monospace fonts, glowing borders) - Chat: added tabbed right sidebar (Console, Tasks, Web Research panels) - Added ConsolePanel component showing tool call output in real-time - Replaced single-line Input with auto-resizing textarea (Shift+Enter for newline) - Auto-switches to Console tab when sending messages - Consistent neon color scheme: cyan=#00D4FF, green=#00FF88, amber=#FFB800, red=#FF3366
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -112,6 +112,11 @@ temp/
|
||||
# Manus version file (auto-generated, not part of source)
|
||||
client/public/__manus__/version.json
|
||||
|
||||
# Secrets — NEVER commit
|
||||
.deploy-secrets
|
||||
deploy-secrets
|
||||
*.secret
|
||||
|
||||
# Kilocode config and working directories
|
||||
.kilo/
|
||||
.manus/
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { AlertCircle, CheckCircle2, Clock, Trash2, Plus } from "lucide-react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Trash2,
|
||||
Plus,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Zap,
|
||||
Loader2,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
|
||||
export interface TasksPanelProps {
|
||||
agentId?: number;
|
||||
conversationId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TasksPanel — правая панель для отображения и управления задачами
|
||||
*/
|
||||
export function TasksPanel({
|
||||
agentId,
|
||||
conversationId,
|
||||
}: TasksPanelProps) {
|
||||
export function TasksPanel({ agentId, conversationId }: TasksPanelProps) {
|
||||
const [tasks, setTasks] = useState<any[]>([]);
|
||||
const [expandedTaskId, setExpandedTaskId] = useState<number | null>(null);
|
||||
|
||||
@@ -50,11 +55,8 @@ export function TasksPanel({
|
||||
...(newStatus === "completed" && { completedAt: new Date() }),
|
||||
...(newStatus === "in_progress" && { startedAt: new Date() }),
|
||||
});
|
||||
|
||||
if (result) {
|
||||
setTasks((prev) =>
|
||||
prev.map((t) => (t.id === taskId ? result : t))
|
||||
);
|
||||
setTasks(prev => prev.map(t => (t.id === taskId ? result : t)));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update task:", error);
|
||||
@@ -65,7 +67,7 @@ export function TasksPanel({
|
||||
try {
|
||||
const success = await deleteTaskMutation.mutateAsync({ taskId });
|
||||
if (success) {
|
||||
setTasks((prev) => prev.filter((t) => t.id !== taskId));
|
||||
setTasks(prev => prev.filter(t => t.id !== taskId));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete task:", error);
|
||||
@@ -75,173 +77,204 @@ export function TasksPanel({
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
|
||||
return <CheckCircle2 className="w-3.5 h-3.5 text-[#00FF88]" />;
|
||||
case "failed":
|
||||
return <AlertCircle className="w-4 h-4 text-red-500" />;
|
||||
return <AlertCircle className="w-3.5 h-3.5 text-[#FF3366]" />;
|
||||
case "in_progress":
|
||||
return <Clock className="w-4 h-4 text-blue-500" />;
|
||||
return <Loader2 className="w-3.5 h-3.5 text-[#00D4FF] animate-spin" />;
|
||||
default:
|
||||
return <Clock className="w-4 h-4 text-gray-400" />;
|
||||
return <Clock className="w-3.5 h-3.5 text-[#FFB800]" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return "bg-green-100 text-green-800";
|
||||
case "failed":
|
||||
return "bg-red-100 text-red-800";
|
||||
case "in_progress":
|
||||
return "bg-blue-100 text-blue-800";
|
||||
case "blocked":
|
||||
return "bg-orange-100 text-orange-800";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
const getStatusBadge = (status: string) => {
|
||||
const styles: Record<string, string> = {
|
||||
completed: "bg-[#00FF88]/10 text-[#00FF88] border-[#00FF88]/30",
|
||||
failed: "bg-[#FF3366]/10 text-[#FF3366] border-[#FF3366]/30",
|
||||
in_progress: "bg-[#00D4FF]/10 text-[#00D4FF] border-[#00D4FF]/30",
|
||||
blocked: "bg-[#FFB800]/10 text-[#FFB800] border-[#FFB800]/30",
|
||||
pending: "bg-[#FFB800]/10 text-[#FFB800]/70 border-[#FFB800]/30",
|
||||
};
|
||||
return (
|
||||
styles[status] ?? "bg-[#FFB800]/10 text-[#FFB800]/70 border-[#FFB800]/30"
|
||||
);
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case "critical":
|
||||
return "bg-red-100 text-red-800";
|
||||
case "high":
|
||||
return "bg-orange-100 text-orange-800";
|
||||
case "medium":
|
||||
return "bg-yellow-100 text-yellow-800";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
const getPriorityBadge = (priority: string) => {
|
||||
const styles: Record<string, string> = {
|
||||
critical: "bg-[#FF3366]/10 text-[#FF3366] border-[#FF3366]/30",
|
||||
high: "bg-[#FFB800]/10 text-[#FFB800] border-[#FFB800]/30",
|
||||
medium: "bg-[#00D4FF]/10 text-[#00D4FF]/70 border-[#00D4FF]/30",
|
||||
low: "bg-muted/20 text-muted-foreground border-border/30",
|
||||
};
|
||||
return (
|
||||
styles[priority] ?? "bg-muted/20 text-muted-foreground border-border/30"
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col bg-white border-l border-gray-200">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Tasks</h2>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="px-3 py-2.5 border-b border-border/30 flex items-center justify-between shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="w-3.5 h-3.5 text-[#FFB800]" />
|
||||
<span className="text-xs font-mono font-semibold text-foreground">
|
||||
TASKS
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[9px] h-4 px-1.5 font-mono border-[#FFB800]/30 text-[#FFB800]/80"
|
||||
>
|
||||
{tasks.length}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
{agentId ? `Agent #${agentId}` : conversationId ? "Conversation" : "No selection"}
|
||||
</p>
|
||||
<span className="text-[9px] font-mono text-muted-foreground/60">
|
||||
{agentId ? `Agent #${agentId}` : conversationId ? "Session" : "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* Task List */}
|
||||
<ScrollArea className="flex-1">
|
||||
{tasks.length === 0 ? (
|
||||
<div className="p-4 text-center text-gray-500 text-sm">
|
||||
No tasks yet
|
||||
<div className="flex flex-col items-center justify-center h-full py-8 gap-2">
|
||||
<Terminal className="w-6 h-6 text-muted-foreground/20" />
|
||||
<p className="text-[10px] font-mono text-muted-foreground/40">
|
||||
No tasks yet
|
||||
</p>
|
||||
<p className="text-[9px] font-mono text-muted-foreground/25">
|
||||
Tasks will appear as the orchestrator works
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 p-4">
|
||||
{tasks.map((task) => (
|
||||
<Card
|
||||
<div className="p-2 space-y-1.5">
|
||||
{tasks.map(task => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="p-3 hover:bg-gray-50 cursor-pointer transition-colors"
|
||||
onClick={() =>
|
||||
setExpandedTaskId(
|
||||
expandedTaskId === task.id ? null : task.id
|
||||
)
|
||||
}
|
||||
className="rounded border border-border/30 bg-secondary/20 hover:bg-secondary/30 transition-colors overflow-hidden"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
checked={task.status === "completed"}
|
||||
onCheckedChange={(checked) => {
|
||||
handleStatusChange(
|
||||
task.id,
|
||||
checked ? "completed" : "pending"
|
||||
);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="mt-1"
|
||||
/>
|
||||
<button
|
||||
className="w-full flex items-start gap-2 px-2.5 py-2 text-left"
|
||||
onClick={() =>
|
||||
setExpandedTaskId(
|
||||
expandedTaskId === task.id ? null : task.id
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="mt-0.5 shrink-0"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={task.status === "completed"}
|
||||
onCheckedChange={checked => {
|
||||
handleStatusChange(
|
||||
task.id,
|
||||
checked ? "completed" : "pending"
|
||||
);
|
||||
}}
|
||||
className="h-3.5 w-3.5 border-border/50 data-[state=checked]:bg-[#00FF88]/20 data-[state=checked]:border-[#00FF88]/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
<p
|
||||
className={`text-[11px] font-mono truncate ${
|
||||
task.status === "completed"
|
||||
? "text-muted-foreground line-through"
|
||||
: "text-foreground/90"
|
||||
}`}
|
||||
>
|
||||
{task.title}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="flex items-center gap-1.5 mt-1">
|
||||
{getStatusIcon(task.status)}
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`text-xs ${getStatusColor(task.status)}`}
|
||||
variant="outline"
|
||||
className={`text-[8px] h-3.5 px-1 font-mono ${getStatusBadge(task.status)}`}
|
||||
>
|
||||
{task.status.replace("_", " ")}
|
||||
</Badge>
|
||||
{task.priority && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`text-xs ${getPriorityColor(task.priority)}`}
|
||||
variant="outline"
|
||||
className={`text-[8px] h-3.5 px-1 font-mono ${getPriorityBadge(task.priority)}`}
|
||||
>
|
||||
{task.priority}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteTask(task.id);
|
||||
}}
|
||||
className="text-gray-400 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleDeleteTask(task.id);
|
||||
}}
|
||||
className="p-0.5 text-muted-foreground/40 hover:text-[#FF3366] transition-colors"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
{expandedTaskId === task.id ? (
|
||||
<ChevronDown className="w-3 h-3 text-muted-foreground/50" />
|
||||
) : (
|
||||
<ChevronRight className="w-3 h-3 text-muted-foreground/50" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{expandedTaskId === task.id && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-200 space-y-2">
|
||||
<div className="px-2.5 pb-2.5 pt-0 space-y-1.5 border-t border-border/20">
|
||||
{task.description && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-600 font-medium">
|
||||
<div className="mt-2">
|
||||
<p className="text-[8px] font-mono text-[#00D4FF]/60 uppercase tracking-wider">
|
||||
Description
|
||||
</p>
|
||||
<p className="text-xs text-gray-700 mt-1">
|
||||
<p className="text-[10px] font-mono text-foreground/60 mt-0.5">
|
||||
{task.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{task.result && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-600 font-medium">
|
||||
<p className="text-[8px] font-mono text-[#00FF88]/60 uppercase tracking-wider">
|
||||
Result
|
||||
</p>
|
||||
<p className="text-xs text-gray-700 mt-1 max-h-20 overflow-y-auto">
|
||||
<pre className="text-[9px] font-mono text-foreground/50 bg-background/50 rounded p-1.5 max-h-20 overflow-auto whitespace-pre-wrap">
|
||||
{task.result}
|
||||
</p>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{task.errorMessage && (
|
||||
<div>
|
||||
<p className="text-xs text-red-600 font-medium">
|
||||
<p className="text-[8px] font-mono text-[#FF3366]/60 uppercase tracking-wider">
|
||||
Error
|
||||
</p>
|
||||
<p className="text-xs text-red-700 mt-1">
|
||||
<p className="text-[9px] font-mono text-[#FF3366]/80">
|
||||
{task.errorMessage}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{task.createdAt && (
|
||||
<div className="text-xs text-gray-500">
|
||||
Created: {new Date(task.createdAt).toLocaleString()}
|
||||
</div>
|
||||
<p className="text-[8px] font-mono text-muted-foreground/30">
|
||||
{new Date(task.createdAt).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="p-4 border-t border-gray-200">
|
||||
{/* Footer */}
|
||||
<div className="px-2.5 py-2 border-t border-border/30 shrink-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
className="w-full h-7 text-[10px] font-mono border-dashed border-[#00D4FF]/30 text-[#00D4FF]/60 hover:text-[#00D4FF] hover:border-[#00D4FF]/60 hover:bg-[#00D4FF]/5 bg-transparent"
|
||||
disabled={!agentId && !conversationId}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
New Task
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { Streamdown } from "streamdown";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { TasksPanel } from "@/components/TasksPanel";
|
||||
import { WebResearchPanel } from "@/components/WebResearchPanel";
|
||||
import {
|
||||
Terminal,
|
||||
Send,
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
Shell,
|
||||
Network,
|
||||
Database,
|
||||
Search,
|
||||
ListChecks,
|
||||
} from "lucide-react";
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
@@ -49,10 +51,16 @@ interface ChatMessage {
|
||||
timestamp: string;
|
||||
toolCalls?: ToolCallStep[];
|
||||
model?: string;
|
||||
usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
|
||||
usage?: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
type SidebarTab = "console" | "tasks" | "research";
|
||||
|
||||
// ─── Tool Icon Map ─────────────────────────────────────────────────────────────
|
||||
|
||||
function ToolIcon({ tool }: { tool: string }) {
|
||||
@@ -68,7 +76,11 @@ function ToolIcon({ tool }: { tool: string }) {
|
||||
install_skill: <Zap className="w-3.5 h-3.5" />,
|
||||
docker_exec: <Code className="w-3.5 h-3.5" />,
|
||||
};
|
||||
return <span className="text-primary">{icons[tool] ?? <Wrench className="w-3.5 h-3.5" />}</span>;
|
||||
return (
|
||||
<span className="text-primary">
|
||||
{icons[tool] ?? <Wrench className="w-3.5 h-3.5" />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function toolLabel(tool: string): string {
|
||||
@@ -94,10 +106,14 @@ function ToolCallCard({ step, index }: { step: ToolCallStep; index: number }) {
|
||||
|
||||
const argsSummary = () => {
|
||||
if (step.tool === "shell_exec") return step.args.command?.slice(0, 60);
|
||||
if (step.tool === "file_read" || step.tool === "file_write") return step.args.path;
|
||||
if (step.tool === "http_request") return `${step.args.method || "GET"} ${step.args.url?.slice(0, 50)}`;
|
||||
if (step.tool === "delegate_to_agent") return `Agent #${step.args.agentId}: ${step.args.message?.slice(0, 40)}`;
|
||||
if (step.tool === "docker_exec") return `docker ${step.args.command?.slice(0, 50)}`;
|
||||
if (step.tool === "file_read" || step.tool === "file_write")
|
||||
return step.args.path;
|
||||
if (step.tool === "http_request")
|
||||
return `${step.args.method || "GET"} ${step.args.url?.slice(0, 50)}`;
|
||||
if (step.tool === "delegate_to_agent")
|
||||
return `Agent #${step.args.agentId}: ${step.args.message?.slice(0, 40)}`;
|
||||
if (step.tool === "docker_exec")
|
||||
return `docker ${step.args.command?.slice(0, 50)}`;
|
||||
return JSON.stringify(step.args).slice(0, 60);
|
||||
};
|
||||
|
||||
@@ -113,14 +129,20 @@ function ToolCallCard({ step, index }: { step: ToolCallStep; index: number }) {
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-secondary/40 transition-colors"
|
||||
>
|
||||
<ToolIcon tool={step.tool} />
|
||||
<span className="text-xs font-mono text-primary font-medium">{toolLabel(step.tool)}</span>
|
||||
<span className="text-xs font-mono text-muted-foreground flex-1 truncate">{argsSummary()}</span>
|
||||
<span className="text-xs font-mono text-primary font-medium">
|
||||
{toolLabel(step.tool)}
|
||||
</span>
|
||||
<span className="text-xs font-mono text-muted-foreground flex-1 truncate">
|
||||
{argsSummary()}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className="text-[10px] font-mono text-muted-foreground">{step.durationMs}ms</span>
|
||||
<span className="text-[10px] font-mono text-muted-foreground">
|
||||
{step.durationMs}ms
|
||||
</span>
|
||||
{step.success ? (
|
||||
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
|
||||
<CheckCircle className="w-3.5 h-3.5 text-[#00FF88]" />
|
||||
) : (
|
||||
<XCircle className="w-3.5 h-3.5 text-red-500" />
|
||||
<XCircle className="w-3.5 h-3.5 text-[#FF3366]" />
|
||||
)}
|
||||
{expanded ? (
|
||||
<ChevronDown className="w-3 h-3 text-muted-foreground" />
|
||||
@@ -133,13 +155,17 @@ function ToolCallCard({ step, index }: { step: ToolCallStep; index: number }) {
|
||||
{expanded && (
|
||||
<div className="px-3 pb-3 space-y-2 border-t border-border/30">
|
||||
<div>
|
||||
<p className="text-[10px] font-mono text-muted-foreground mt-2 mb-1">INPUT</p>
|
||||
<p className="text-[10px] font-mono text-[#00D4FF]/50 mt-2 mb-1">
|
||||
INPUT
|
||||
</p>
|
||||
<pre className="text-[10px] font-mono text-foreground/80 bg-background/50 rounded p-2 overflow-auto max-h-32 whitespace-pre-wrap">
|
||||
{JSON.stringify(step.args, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-mono text-muted-foreground mb-1">OUTPUT</p>
|
||||
<p className="text-[10px] font-mono text-[#00FF88]/50 mb-1">
|
||||
OUTPUT
|
||||
</p>
|
||||
<pre className="text-[10px] font-mono text-foreground/80 bg-background/50 rounded p-2 overflow-auto max-h-48 whitespace-pre-wrap">
|
||||
{step.success
|
||||
? typeof step.result === "string"
|
||||
@@ -172,26 +198,35 @@ function MessageBubble({ msg }: { msg: ChatMessage }) {
|
||||
isUser
|
||||
? "bg-primary/15 border-primary/30"
|
||||
: isSystem
|
||||
? "bg-yellow-500/10 border-yellow-500/30"
|
||||
: "bg-cyan-500/10 border-cyan-500/30"
|
||||
? "bg-[#FFB800]/10 border-[#FFB800]/30"
|
||||
: "bg-[#00D4FF]/10 border-[#00D4FF]/30"
|
||||
}`}
|
||||
>
|
||||
{isUser ? (
|
||||
<User className="w-3.5 h-3.5 text-primary" />
|
||||
) : isSystem ? (
|
||||
<Zap className="w-3.5 h-3.5 text-yellow-500" />
|
||||
<Zap className="w-3.5 h-3.5 text-[#FFB800]" />
|
||||
) : (
|
||||
<Bot className="w-3.5 h-3.5 text-cyan-400" />
|
||||
<Bot className="w-3.5 h-3.5 text-[#00D4FF]" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className={`flex-1 min-w-0 ${isUser ? "items-end" : "items-start"} flex flex-col gap-1`}>
|
||||
<div
|
||||
className={`flex-1 min-w-0 ${isUser ? "items-end" : "items-start"} flex flex-col gap-1`}
|
||||
>
|
||||
{/* Meta */}
|
||||
<div className={`flex items-center gap-2 ${isUser ? "flex-row-reverse" : ""}`}>
|
||||
<span className="text-[10px] font-mono text-muted-foreground">{msg.timestamp}</span>
|
||||
<div
|
||||
className={`flex items-center gap-2 ${isUser ? "flex-row-reverse" : ""}`}
|
||||
>
|
||||
<span className="text-[10px] font-mono text-muted-foreground">
|
||||
{msg.timestamp}
|
||||
</span>
|
||||
{msg.model && (
|
||||
<Badge variant="outline" className="text-[9px] h-4 px-1.5 font-mono border-primary/30 text-primary">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[9px] h-4 px-1.5 font-mono border-[#00D4FF]/30 text-[#00D4FF]/80"
|
||||
>
|
||||
{msg.model}
|
||||
</Badge>
|
||||
)}
|
||||
@@ -207,7 +242,8 @@ function MessageBubble({ msg }: { msg: ChatMessage }) {
|
||||
<div className="w-full space-y-1 mb-1">
|
||||
<p className="text-[10px] font-mono text-muted-foreground flex items-center gap-1">
|
||||
<Wrench className="w-3 h-3" />
|
||||
{msg.toolCalls.length} tool call{msg.toolCalls.length > 1 ? "s" : ""}
|
||||
{msg.toolCalls.length} tool call
|
||||
{msg.toolCalls.length > 1 ? "s" : ""}
|
||||
</p>
|
||||
{msg.toolCalls.map((step, i) => (
|
||||
<ToolCallCard key={i} step={step} index={i} />
|
||||
@@ -222,14 +258,16 @@ function MessageBubble({ msg }: { msg: ChatMessage }) {
|
||||
isUser
|
||||
? "bg-primary/15 border border-primary/20 text-foreground"
|
||||
: isSystem
|
||||
? "bg-yellow-500/10 border border-yellow-500/20 text-yellow-200"
|
||||
: msg.isError
|
||||
? "bg-red-500/10 border border-red-500/20 text-red-300"
|
||||
: "bg-secondary/50 border border-border/40 text-foreground"
|
||||
? "bg-[#FFB800]/10 border border-[#FFB800]/20 text-[#FFB800]"
|
||||
: msg.isError
|
||||
? "bg-[#FF3366]/10 border border-[#FF3366]/20 text-[#FF3366]"
|
||||
: "bg-secondary/50 border border-border/40 text-foreground"
|
||||
}`}
|
||||
>
|
||||
{isUser || isSystem || msg.isError ? (
|
||||
<pre className="font-mono text-xs whitespace-pre-wrap break-words">{msg.content}</pre>
|
||||
<pre className="font-mono text-xs whitespace-pre-wrap break-words">
|
||||
{msg.content}
|
||||
</pre>
|
||||
) : (
|
||||
<div className="text-sm prose prose-invert prose-sm max-w-none">
|
||||
<Streamdown>{msg.content}</Streamdown>
|
||||
@@ -242,6 +280,139 @@ function MessageBubble({ msg }: { msg: ChatMessage }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Console Panel ────────────────────────────────────────────────────────────
|
||||
|
||||
function ConsolePanel({ messages }: { messages: ChatMessage[] }) {
|
||||
const consoleRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (consoleRef.current) {
|
||||
consoleRef.current.scrollTop = consoleRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
const consoleEntries = messages.filter(
|
||||
m => m.toolCalls && m.toolCalls.length > 0
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="px-3 py-2.5 border-b border-border/30 flex items-center justify-between shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="w-3.5 h-3.5 text-[#00FF88]" />
|
||||
<span className="text-xs font-mono font-semibold text-foreground">
|
||||
CONSOLE
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[9px] h-4 px-1.5 font-mono border-[#00FF88]/30 text-[#00FF88]/80"
|
||||
>
|
||||
{consoleEntries.reduce(
|
||||
(acc, m) => acc + (m.toolCalls?.length ?? 0),
|
||||
0
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
<span className="text-[9px] font-mono text-muted-foreground/60">
|
||||
tool output
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={consoleRef}
|
||||
className="flex-1 overflow-y-auto p-2 space-y-1.5 font-mono text-[10px]"
|
||||
>
|
||||
{consoleEntries.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full py-8 gap-2">
|
||||
<Terminal className="w-6 h-6 text-muted-foreground/20" />
|
||||
<p className="text-[10px] text-muted-foreground/40">
|
||||
No tool output yet
|
||||
</p>
|
||||
<p className="text-[9px] text-muted-foreground/25">
|
||||
Tool calls will appear here
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
consoleEntries.map(msg =>
|
||||
msg.toolCalls?.map((step, i) => (
|
||||
<div
|
||||
key={`${msg.id}-tool-${i}`}
|
||||
className="rounded border border-border/30 bg-secondary/20 overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center gap-2 px-2 py-1.5 bg-secondary/30">
|
||||
<ToolIcon tool={step.tool} />
|
||||
<span className="text-[#00D4FF]/80 font-medium">
|
||||
{toolLabel(step.tool)}
|
||||
</span>
|
||||
<span className="text-muted-foreground/50 flex-1 truncate">
|
||||
{step.args.command || step.args.path || ""}
|
||||
</span>
|
||||
<span className="text-muted-foreground/40">
|
||||
{step.durationMs}ms
|
||||
</span>
|
||||
{step.success ? (
|
||||
<CheckCircle className="w-3 h-3 text-[#00FF88]" />
|
||||
) : (
|
||||
<XCircle className="w-3 h-3 text-[#FF3366]" />
|
||||
)}
|
||||
</div>
|
||||
<pre className="px-2 py-1.5 text-foreground/60 bg-background/30 max-h-20 overflow-auto whitespace-pre-wrap break-words">
|
||||
{step.success
|
||||
? typeof step.result === "string"
|
||||
? step.result.slice(0, 500)
|
||||
: JSON.stringify(step.result, null, 2).slice(0, 500)
|
||||
: `ERROR: ${step.result?.error ?? "Unknown"}`}
|
||||
</pre>
|
||||
</div>
|
||||
))
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Sidebar Tab Button ──────────────────────────────────────────────────────
|
||||
|
||||
function SidebarTabButton({
|
||||
active,
|
||||
onClick,
|
||||
icon,
|
||||
label,
|
||||
count,
|
||||
}: {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
count?: number;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 text-[10px] font-mono uppercase tracking-wider transition-colors border-b-2 ${
|
||||
active
|
||||
? "text-foreground border-[#00D4FF] bg-[#00D4FF]/5"
|
||||
: "text-muted-foreground/50 border-transparent hover:text-muted-foreground hover:border-border/50"
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
{count !== undefined && count > 0 && (
|
||||
<span
|
||||
className={`ml-0.5 text-[8px] px-1 rounded-full ${
|
||||
active
|
||||
? "bg-[#00D4FF]/20 text-[#00D4FF]"
|
||||
: "bg-muted/30 text-muted-foreground/40"
|
||||
}`}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main Chat Component ──────────────────────────────────────────────────────
|
||||
|
||||
export default function Chat() {
|
||||
@@ -252,16 +423,21 @@ export default function Chat() {
|
||||
const [input, setInput] = useState("");
|
||||
const [isThinking, setIsThinking] = useState(false);
|
||||
const [retryAttempt, setRetryAttempt] = useState(0);
|
||||
const [lastError, setLastError] = useState<{ message: string; isRetryable: boolean } | null>(null);
|
||||
const [lastError, setLastError] = useState<{
|
||||
message: string;
|
||||
isRetryable: boolean;
|
||||
} | null>(null);
|
||||
const [conversationId] = useState(`conv-${Date.now()}`);
|
||||
const [activeTab, setActiveTab] = useState<SidebarTab>("console");
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const agentsQuery = trpc.agents.list.useQuery(undefined, { refetchInterval: 30000 });
|
||||
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;
|
||||
@@ -270,7 +446,10 @@ export default function Chat() {
|
||||
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" }),
|
||||
timestamp: new Date().toLocaleTimeString("ru-RU", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -282,8 +461,24 @@ export default function Chat() {
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
// Auto-resize textarea
|
||||
const adjustTextareaHeight = useCallback(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = "auto";
|
||||
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 150)}px`;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
adjustTextareaHeight();
|
||||
}, [input, adjustTextareaHeight]);
|
||||
|
||||
const getTs = () =>
|
||||
new Date().toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
new Date().toLocaleTimeString("ru-RU", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!input.trim() || isThinking) return;
|
||||
@@ -291,14 +486,13 @@ export default function Chat() {
|
||||
const userContent = input.trim();
|
||||
const ts = getTs();
|
||||
|
||||
// Add user message
|
||||
const userMsg: ChatMessage = {
|
||||
id: `user-${Date.now()}`,
|
||||
role: "user",
|
||||
content: userContent,
|
||||
timestamp: ts,
|
||||
};
|
||||
setMessages((prev) => [...prev, userMsg]);
|
||||
setMessages(prev => [...prev, userMsg]);
|
||||
|
||||
const newHistory = [
|
||||
...conversationHistory,
|
||||
@@ -308,9 +502,11 @@ export default function Chat() {
|
||||
setInput("");
|
||||
setIsThinking(true);
|
||||
|
||||
// Add thinking indicator
|
||||
// Switch to console tab when sending a message
|
||||
setActiveTab("console");
|
||||
|
||||
const thinkingId = `thinking-${Date.now()}`;
|
||||
setMessages((prev) => [
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{
|
||||
id: thinkingId,
|
||||
@@ -323,28 +519,23 @@ export default function Chat() {
|
||||
try {
|
||||
const result = await orchestratorMutation.mutateAsync({
|
||||
messages: newHistory,
|
||||
// model is loaded from DB config — do not override here
|
||||
maxIterations: 10,
|
||||
});
|
||||
|
||||
// Remove thinking indicator
|
||||
setMessages((prev) => prev.filter((m) => m.id !== thinkingId));
|
||||
setMessages(prev => prev.filter(m => m.id !== thinkingId));
|
||||
|
||||
const respTs = getTs();
|
||||
|
||||
// Clear error state on success
|
||||
setLastError(null);
|
||||
setRetryAttempt(0);
|
||||
|
||||
if (result.success) {
|
||||
// Update conversation history
|
||||
setConversationHistory((prev) => [
|
||||
setConversationHistory(prev => [
|
||||
...prev,
|
||||
{ role: "assistant" as const, content: result.response },
|
||||
]);
|
||||
|
||||
// Add assistant message with tool calls
|
||||
setMessages((prev) => [
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{
|
||||
id: `resp-${Date.now()}`,
|
||||
@@ -357,7 +548,7 @@ export default function Chat() {
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setMessages((prev) => [
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{
|
||||
id: `err-${Date.now()}`,
|
||||
@@ -369,14 +560,17 @@ export default function Chat() {
|
||||
]);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setMessages((prev) => prev.filter((m) => m.id !== thinkingId));
|
||||
setMessages(prev => prev.filter(m => m.id !== thinkingId));
|
||||
const errorMsg = err.message || "Unknown error";
|
||||
const isRetryable = errorMsg.includes("timeout") || errorMsg.includes("unavailable") || errorMsg.includes("ECONNREFUSED");
|
||||
|
||||
const isRetryable =
|
||||
errorMsg.includes("timeout") ||
|
||||
errorMsg.includes("unavailable") ||
|
||||
errorMsg.includes("ECONNREFUSED");
|
||||
|
||||
setLastError({ message: errorMsg, isRetryable });
|
||||
setRetryAttempt((prev) => prev + 1);
|
||||
|
||||
setMessages((prev) => [
|
||||
setRetryAttempt(prev => prev + 1);
|
||||
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{
|
||||
id: `err-${Date.now()}`,
|
||||
@@ -386,29 +580,46 @@ export default function Chat() {
|
||||
isError: true,
|
||||
},
|
||||
]);
|
||||
|
||||
// Auto-retry if retryable and under max attempts
|
||||
|
||||
if (isRetryable && retryAttempt < 2) {
|
||||
setTimeout(() => {
|
||||
sendMessage();
|
||||
}, 1000 * Math.pow(2, retryAttempt));
|
||||
setTimeout(
|
||||
() => {
|
||||
sendMessage();
|
||||
},
|
||||
1000 * Math.pow(2, retryAttempt)
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsThinking(false);
|
||||
setTimeout(() => inputRef.current?.focus(), 100);
|
||||
} };
|
||||
setTimeout(() => textareaRef.current?.focus(), 100);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const agents = agentsQuery.data ?? [];
|
||||
const activeAgents = agents.filter((a) => a.isActive && !(a as any).isOrchestrator);
|
||||
const activeAgents = agents.filter(
|
||||
a => a.isActive && !(a as any).isOrchestrator
|
||||
);
|
||||
const orchConfig = orchestratorConfigQuery.data;
|
||||
|
||||
const toolCallCount = messages.reduce(
|
||||
(acc, m) => acc + (m.toolCalls?.length ?? 0),
|
||||
0
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-3">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-md bg-cyan-500/15 border border-cyan-500/30 flex items-center justify-center">
|
||||
<Bot className="w-4 h-4 text-cyan-400" />
|
||||
<div className="w-8 h-8 rounded-md bg-[#00D4FF]/15 border border-[#00D4FF]/30 flex items-center justify-center">
|
||||
<Bot className="w-4 h-4 text-[#00D4FF]" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-foreground">
|
||||
@@ -417,8 +628,10 @@ export default function Chat() {
|
||||
<p className="text-[11px] font-mono text-muted-foreground">
|
||||
{orchConfig ? (
|
||||
<span>
|
||||
<span className="text-cyan-400/80">{orchConfig.model}</span>
|
||||
{" · "}{activeAgents.length} agents · {ORCHESTRATOR_TOOLS_COUNT} tools
|
||||
<span className="text-[#00D4FF]/80">{orchConfig.model}</span>
|
||||
{" · "}
|
||||
{activeAgents.length} agents · {ORCHESTRATOR_TOOLS_COUNT}{" "}
|
||||
tools
|
||||
</span>
|
||||
) : (
|
||||
`Main AI · ${activeAgents.length} agents · ${ORCHESTRATOR_TOOLS_COUNT} tools`
|
||||
@@ -427,25 +640,30 @@ export default function Chat() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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) => (
|
||||
{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.role === "browser" && (
|
||||
<Globe className="w-2.5 h-2.5 mr-1 text-[#00D4FF]" />
|
||||
)}
|
||||
{agent.role === "tool_builder" && (
|
||||
<Wrench className="w-2.5 h-2.5 mr-1 text-[#FFB800]" />
|
||||
)}
|
||||
{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"
|
||||
className="text-[10px] font-mono px-2 py-1 rounded border border-[#00D4FF]/30 text-[#00D4FF]/70 hover:text-[#00D4FF] hover:border-[#00D4FF]/60 transition-colors bg-[#00D4FF]/5 shrink-0"
|
||||
>
|
||||
Configure
|
||||
</a>
|
||||
@@ -457,85 +675,124 @@ export default function Chat() {
|
||||
{/* Chat area */}
|
||||
<Card className="flex-1 bg-card border-border/50 overflow-hidden">
|
||||
<CardContent className="p-0 h-full flex flex-col">
|
||||
<ScrollArea className="flex-1">
|
||||
<div ref={scrollRef} className="p-4 space-y-4">
|
||||
<AnimatePresence initial={false}>
|
||||
{messages.map((msg) => (
|
||||
<MessageBubble key={msg.id} msg={msg} />
|
||||
))}
|
||||
</AnimatePresence>
|
||||
<ScrollArea className="flex-1">
|
||||
<div ref={scrollRef} className="p-4 space-y-4">
|
||||
<AnimatePresence initial={false}>
|
||||
{messages.map(msg => (
|
||||
<MessageBubble key={msg.id} msg={msg} />
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Thinking indicator */}
|
||||
{isThinking && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex items-center gap-2 text-cyan-400 font-mono text-xs pl-10"
|
||||
>
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
<span className="text-muted-foreground">Orchestrator thinking...</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Input area */}
|
||||
<div className="border-t border-border/50 p-3 bg-secondary/10 shrink-0">
|
||||
{/* Quick commands */}
|
||||
<div className="flex items-center gap-1.5 mb-2 flex-wrap">
|
||||
{[
|
||||
"Список агентов",
|
||||
"Покажи файлы проекта",
|
||||
"Статус Docker",
|
||||
"Создай инструмент",
|
||||
"Скомпилируй агента",
|
||||
].map((cmd) => (
|
||||
<button
|
||||
key={cmd}
|
||||
onClick={() => setInput(cmd)}
|
||||
className="text-[10px] font-mono px-2 py-0.5 rounded border border-border/40 text-muted-foreground hover:text-foreground hover:border-primary/40 transition-colors bg-secondary/20"
|
||||
>
|
||||
{cmd}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-cyan-400 font-mono text-sm shrink-0">$</span>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && sendMessage()}
|
||||
placeholder={isThinking ? "Ожидание ответа оркестратора..." : "Введите команду или вопрос..."}
|
||||
disabled={isThinking}
|
||||
className="bg-transparent border-none text-foreground font-mono text-sm placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0 h-8"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={sendMessage}
|
||||
disabled={isThinking || !input.trim()}
|
||||
className="bg-cyan-500/15 text-cyan-400 border border-cyan-500/30 hover:bg-cyan-500/25 h-8 w-8 p-0 shrink-0"
|
||||
>
|
||||
{isThinking ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
{isThinking && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex items-center gap-2 text-[#00D4FF] font-mono text-xs pl-10"
|
||||
>
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
<span className="text-muted-foreground">
|
||||
Orchestrator thinking...
|
||||
</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Tasks Panel */}
|
||||
<div className="w-80 border-l border-border/30 bg-secondary/5 rounded-lg overflow-hidden">
|
||||
<TasksPanel conversationId={conversationId} />
|
||||
{/* Input area */}
|
||||
<div className="border-t border-border/50 p-3 bg-secondary/10 shrink-0">
|
||||
{/* Quick commands */}
|
||||
<div className="flex items-center gap-1.5 mb-2 flex-wrap">
|
||||
{[
|
||||
"Список агентов",
|
||||
"Покажи файлы проекта",
|
||||
"Статус Docker",
|
||||
"Создай инструмент",
|
||||
"Скомпилируй агента",
|
||||
].map(cmd => (
|
||||
<button
|
||||
key={cmd}
|
||||
onClick={() => setInput(cmd)}
|
||||
className="text-[10px] font-mono px-2 py-0.5 rounded border border-border/40 text-muted-foreground hover:text-foreground hover:border-[#00D4FF]/40 transition-colors bg-secondary/20"
|
||||
>
|
||||
{cmd}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-end gap-2">
|
||||
<span className="text-[#00D4FF] font-mono text-sm shrink-0 pb-1">
|
||||
$
|
||||
</span>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
isThinking
|
||||
? "Ожидание ответа оркестратора..."
|
||||
: "Введите команду или вопрос... (Shift+Enter для новой строки)"
|
||||
}
|
||||
disabled={isThinking}
|
||||
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()}
|
||||
className="bg-[#00D4FF]/15 text-[#00D4FF] border border-[#00D4FF]/30 hover:bg-[#00D4FF]/25 h-8 w-8 p-0 shrink-0"
|
||||
>
|
||||
{isThinking ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Right Sidebar — Tabbed Panel */}
|
||||
<div className="w-80 border border-border/30 bg-card rounded-lg overflow-hidden flex flex-col">
|
||||
{/* Tab bar */}
|
||||
<div className="flex border-b border-border/30 shrink-0">
|
||||
<SidebarTabButton
|
||||
active={activeTab === "console"}
|
||||
onClick={() => setActiveTab("console")}
|
||||
icon={<Terminal className="w-3 h-3" />}
|
||||
label="Console"
|
||||
count={toolCallCount}
|
||||
/>
|
||||
<SidebarTabButton
|
||||
active={activeTab === "tasks"}
|
||||
onClick={() => setActiveTab("tasks")}
|
||||
icon={<ListChecks className="w-3 h-3" />}
|
||||
label="Tasks"
|
||||
/>
|
||||
<SidebarTabButton
|
||||
active={activeTab === "research"}
|
||||
onClick={() => setActiveTab("research")}
|
||||
icon={<Search className="w-3 h-3" />}
|
||||
label="Research"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="flex-1 min-h-0">
|
||||
{activeTab === "console" && <ConsolePanel messages={messages} />}
|
||||
{activeTab === "tasks" && (
|
||||
<TasksPanel conversationId={conversationId} />
|
||||
)}
|
||||
{activeTab === "research" && (
|
||||
<WebResearchPanel conversationId={conversationId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Count of orchestrator tools (used in header)
|
||||
const ORCHESTRATOR_TOOLS_COUNT = 10;
|
||||
|
||||
Reference in New Issue
Block a user