feat(chat): 3-panel layout with conversation list, agent activity tracker, and persistence
- Restored chatStore.ts: localStorage-persisted conversations that survive page navigation - Left panel: conversation list with create/delete/switch, message counts, last message preview - Center: chat messages with original Mission Control dark theme - Right panel: tabbed Console/Tasks/Research with agent activity tracking - Console tab now shows active agents with live elapsed timer, container ID, and status - AgentActivity cards with real-time pulse animation and ping indicator - Research agent tracking: auto-creates activity card when web research is running - Conversations persist across page navigation (localStorage goclaw-conversations-v4) - New conversations auto-title from first user message
This commit is contained in:
359
client/src/lib/chatStore.ts
Normal file
359
client/src/lib/chatStore.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* chatStore — Global singleton for chat conversations.
|
||||
*
|
||||
* Stores conversations in localStorage, supports:
|
||||
* - Create/delete/switch conversations
|
||||
* - Persist messages across navigation
|
||||
* - Uses synchronous orchestrator.chat tRPC endpoint
|
||||
*/
|
||||
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ToolCallStep {
|
||||
tool: string;
|
||||
args: Record<string, any>;
|
||||
result: any;
|
||||
success: boolean;
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: "user" | "assistant" | "system";
|
||||
content: string;
|
||||
timestamp: string;
|
||||
toolCalls?: ToolCallStep[];
|
||||
model?: string;
|
||||
usage?: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
id: string;
|
||||
title: string;
|
||||
createdAt: number;
|
||||
messages: ChatMessage[];
|
||||
history: Array<{ role: "user" | "assistant" | "system"; content: string }>;
|
||||
}
|
||||
|
||||
// ─── Active Agent Tracking ───────────────────────────────────────────────────
|
||||
|
||||
export interface AgentActivity {
|
||||
id: string;
|
||||
agentName: string;
|
||||
agentRole: string;
|
||||
containerId?: string;
|
||||
startedAt: number;
|
||||
status: "running" | "done" | "error";
|
||||
result?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
type StoreEvent = "update";
|
||||
type UpdateHandler = () => void;
|
||||
|
||||
// ─── Persistence ──────────────────────────────────────────────────────────────
|
||||
|
||||
const STORAGE_KEY = "goclaw-conversations-v4";
|
||||
|
||||
function loadConversations(): Conversation[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return [];
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function persistConversations(convs: Conversation[]) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(convs.slice(0, 50)));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function getTs(): string {
|
||||
return new Date().toLocaleTimeString("ru-RU", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Store ────────────────────────────────────────────────────────────────────
|
||||
|
||||
class ChatStore {
|
||||
private conversations: Conversation[] = loadConversations();
|
||||
private activeId: string = "";
|
||||
private isThinking = false;
|
||||
private activeAgents: AgentActivity[] = [];
|
||||
private listeners = new Set<UpdateHandler>();
|
||||
|
||||
constructor() {
|
||||
if (this.conversations.length > 0) {
|
||||
this.activeId = this.conversations[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Subscriptions ──────────────────────────────────────────────────────────
|
||||
|
||||
on(_event: "update", handler: UpdateHandler): void {
|
||||
this.listeners.add(handler);
|
||||
}
|
||||
|
||||
off(_event: "update", handler: UpdateHandler): void {
|
||||
this.listeners.delete(handler);
|
||||
}
|
||||
|
||||
private emit() {
|
||||
this.listeners.forEach(h => h());
|
||||
}
|
||||
|
||||
// ─── Selectors ──────────────────────────────────────────────────────────────
|
||||
|
||||
getConversations(): Conversation[] {
|
||||
return this.conversations;
|
||||
}
|
||||
getActiveId(): string {
|
||||
return this.activeId;
|
||||
}
|
||||
getActive(): Conversation | null {
|
||||
return this.conversations.find(c => c.id === this.activeId) ?? null;
|
||||
}
|
||||
getIsThinking(): boolean {
|
||||
return this.isThinking;
|
||||
}
|
||||
|
||||
// ─── Mutations ──────────────────────────────────────────────────────────────
|
||||
|
||||
setActiveId(id: string) {
|
||||
this.activeId = id;
|
||||
this.emit();
|
||||
}
|
||||
|
||||
createConversation(orchName = "GoClaw Orchestrator"): string {
|
||||
const id = `conv-${nanoid(8)}`;
|
||||
const welcome: ChatMessage = {
|
||||
id: "welcome",
|
||||
role: "system",
|
||||
content: `${orchName} ready.\nI have access to all agents, tools, and skills.\nType a command or ask anything.`,
|
||||
timestamp: getTs(),
|
||||
};
|
||||
const conv: Conversation = {
|
||||
id,
|
||||
title: "New Chat",
|
||||
createdAt: Date.now(),
|
||||
messages: [welcome],
|
||||
history: [],
|
||||
};
|
||||
this.conversations = [conv, ...this.conversations];
|
||||
this.activeId = id;
|
||||
persistConversations(this.conversations);
|
||||
this.emit();
|
||||
return id;
|
||||
}
|
||||
|
||||
deleteConversation(id: string) {
|
||||
this.conversations = this.conversations.filter(c => c.id !== id);
|
||||
if (this.activeId === id) {
|
||||
if (this.conversations.length > 0) {
|
||||
this.activeId = this.conversations[0].id;
|
||||
} else {
|
||||
this.createConversation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
persistConversations(this.conversations);
|
||||
this.emit();
|
||||
}
|
||||
|
||||
setThinking(v: boolean) {
|
||||
this.isThinking = v;
|
||||
this.emit();
|
||||
}
|
||||
|
||||
/** Add a user message to the active conversation. Returns the conv ID. */
|
||||
addUserMessage(
|
||||
content: string,
|
||||
convId?: string
|
||||
): {
|
||||
convId: string;
|
||||
userMsg: ChatMessage;
|
||||
newHistory: Array<{
|
||||
role: "user" | "assistant" | "system";
|
||||
content: string;
|
||||
}>;
|
||||
} {
|
||||
let id = convId ?? this.activeId;
|
||||
if (!id || !this.conversations.find(c => c.id === id)) {
|
||||
id = this.createConversation();
|
||||
}
|
||||
|
||||
const userMsg: ChatMessage = {
|
||||
id: `user-${Date.now()}`,
|
||||
role: "user",
|
||||
content: content.trim(),
|
||||
timestamp: getTs(),
|
||||
};
|
||||
|
||||
const conv = this.conversations.find(c => c.id === id);
|
||||
const newHistory = [
|
||||
...(conv?.history ?? []),
|
||||
{ role: "user" as const, content: content.trim() },
|
||||
];
|
||||
|
||||
this.conversations = this.conversations.map(c =>
|
||||
c.id === id
|
||||
? {
|
||||
...c,
|
||||
title:
|
||||
c.history.length === 0
|
||||
? content.trim().slice(0, 40) + (content.length > 40 ? "…" : "")
|
||||
: c.title,
|
||||
messages: [...c.messages, userMsg],
|
||||
history: newHistory,
|
||||
}
|
||||
: c
|
||||
);
|
||||
persistConversations(this.conversations);
|
||||
this.emit();
|
||||
return { convId: id, userMsg, newHistory };
|
||||
}
|
||||
|
||||
/** Add a thinking/processing placeholder. Returns thinkingMsgId. */
|
||||
addThinkingMessage(convId: string): string {
|
||||
const thinkingId = `thinking-${Date.now()}`;
|
||||
const thinkingMsg: ChatMessage = {
|
||||
id: thinkingId,
|
||||
role: "system",
|
||||
content: "Orchestrator is processing...",
|
||||
timestamp: getTs(),
|
||||
};
|
||||
|
||||
this.conversations = this.conversations.map(c =>
|
||||
c.id === convId ? { ...c, messages: [...c.messages, thinkingMsg] } : c
|
||||
);
|
||||
persistConversations(this.conversations);
|
||||
this.emit();
|
||||
return thinkingId;
|
||||
}
|
||||
|
||||
/** Remove the thinking placeholder. */
|
||||
removeThinkingMessage(convId: string, thinkingId: string) {
|
||||
this.conversations = this.conversations.map(c =>
|
||||
c.id === convId
|
||||
? { ...c, messages: c.messages.filter(m => m.id !== thinkingId) }
|
||||
: c
|
||||
);
|
||||
this.emit();
|
||||
}
|
||||
|
||||
/** Add assistant response after successful chat. */
|
||||
addAssistantMessage(
|
||||
convId: string,
|
||||
response: string,
|
||||
toolCalls: any[] | undefined,
|
||||
model: string | undefined,
|
||||
usage: any | undefined,
|
||||
history: Array<{ role: "user" | "assistant" | "system"; content: string }>
|
||||
) {
|
||||
const msg: ChatMessage = {
|
||||
id: `resp-${Date.now()}`,
|
||||
role: "assistant",
|
||||
content: response,
|
||||
timestamp: getTs(),
|
||||
toolCalls,
|
||||
model,
|
||||
usage,
|
||||
};
|
||||
|
||||
this.conversations = this.conversations.map(c =>
|
||||
c.id === convId
|
||||
? { ...c, messages: [...c.messages, msg], history: history }
|
||||
: c
|
||||
);
|
||||
persistConversations(this.conversations);
|
||||
this.isThinking = false;
|
||||
this.emit();
|
||||
}
|
||||
|
||||
/** Add error message on failed chat. */
|
||||
addErrorMessage(convId: string, errorMsg: string) {
|
||||
const msg: ChatMessage = {
|
||||
id: `err-${Date.now()}`,
|
||||
role: "assistant",
|
||||
content: errorMsg,
|
||||
timestamp: getTs(),
|
||||
isError: true,
|
||||
};
|
||||
|
||||
this.conversations = this.conversations.map(c =>
|
||||
c.id === convId ? { ...c, messages: [...c.messages, msg] } : c
|
||||
);
|
||||
persistConversations(this.conversations);
|
||||
this.isThinking = false;
|
||||
this.emit();
|
||||
}
|
||||
|
||||
// ─── Active Agent Tracking ──────────────────────────────────────────────────
|
||||
|
||||
getActiveAgents(): AgentActivity[] {
|
||||
return this.activeAgents;
|
||||
}
|
||||
|
||||
/** Start tracking an agent activity. Returns the activity id. */
|
||||
startAgentActivity(
|
||||
agentName: string,
|
||||
agentRole: string,
|
||||
containerId?: string
|
||||
): string {
|
||||
const id = `agent-${nanoid(6)}`;
|
||||
const activity: AgentActivity = {
|
||||
id,
|
||||
agentName,
|
||||
agentRole,
|
||||
containerId,
|
||||
startedAt: Date.now(),
|
||||
status: "running",
|
||||
};
|
||||
this.activeAgents = [...this.activeAgents, activity];
|
||||
this.emit();
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Update agent activity status. */
|
||||
updateAgentActivity(
|
||||
activityId: string,
|
||||
updates: Partial<
|
||||
Pick<AgentActivity, "status" | "result" | "error" | "containerId">
|
||||
>
|
||||
) {
|
||||
this.activeAgents = this.activeAgents.map(a =>
|
||||
a.id === activityId ? { ...a, ...updates } : a
|
||||
);
|
||||
this.emit();
|
||||
}
|
||||
|
||||
/** Remove a completed agent activity. */
|
||||
removeAgentActivity(activityId: string) {
|
||||
this.activeAgents = this.activeAgents.filter(a => a.id !== activityId);
|
||||
this.emit();
|
||||
}
|
||||
|
||||
/** Clear all agent activities. */
|
||||
clearAgentActivities() {
|
||||
this.activeAgents = [];
|
||||
this.emit();
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton
|
||||
export const chatStore = new ChatStore();
|
||||
@@ -2,6 +2,12 @@ import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { Streamdown } from "streamdown";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import {
|
||||
chatStore,
|
||||
type ChatMessage,
|
||||
type Conversation,
|
||||
type AgentActivity,
|
||||
} from "@/lib/chatStore";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -21,46 +27,65 @@ import {
|
||||
ChevronRight,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
Zap,
|
||||
Code,
|
||||
FileText,
|
||||
Shell,
|
||||
Network,
|
||||
Database,
|
||||
Plus,
|
||||
Trash2,
|
||||
MessageSquare,
|
||||
Search,
|
||||
ListChecks,
|
||||
Radio,
|
||||
Clock,
|
||||
Activity,
|
||||
Container,
|
||||
} from "lucide-react";
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface ToolCallStep {
|
||||
tool: string;
|
||||
args: Record<string, any>;
|
||||
result: any;
|
||||
success: boolean;
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
type MessageRole = "user" | "assistant" | "system";
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
role: MessageRole;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
toolCalls?: ToolCallStep[];
|
||||
model?: string;
|
||||
usage?: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
type SidebarTab = "console" | "tasks" | "research";
|
||||
|
||||
// ─── useChatStore hook ────────────────────────────────────────────────────────
|
||||
|
||||
function useChatStore() {
|
||||
const [, forceRender] = useState(0);
|
||||
useEffect(() => {
|
||||
const handler = () => forceRender(n => n + 1);
|
||||
chatStore.on("update", handler);
|
||||
return () => chatStore.off("update", handler);
|
||||
}, []);
|
||||
return {
|
||||
conversations: chatStore.getConversations(),
|
||||
activeId: chatStore.getActiveId(),
|
||||
active: chatStore.getActive(),
|
||||
isThinking: chatStore.getIsThinking(),
|
||||
activeAgents: chatStore.getActiveAgents(),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Live Timer Hook ──────────────────────────────────────────────────────────
|
||||
|
||||
function useElapsedTime(startedAt: number, status: string): string {
|
||||
const [elapsed, setElapsed] = useState(() =>
|
||||
status === "running" ? Math.floor((Date.now() - startedAt) / 1000) : 0
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (status !== "running") return;
|
||||
const interval = setInterval(() => {
|
||||
setElapsed(Math.floor((Date.now() - startedAt) / 1000));
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [startedAt, status]);
|
||||
|
||||
if (status !== "running") return "";
|
||||
const m = Math.floor(elapsed / 60);
|
||||
const s = elapsed % 60;
|
||||
return `${m}:${s.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
// ─── Tool Icon Map ─────────────────────────────────────────────────────────────
|
||||
|
||||
function ToolIcon({ tool }: { tool: string }) {
|
||||
@@ -101,7 +126,7 @@ function toolLabel(tool: string): string {
|
||||
|
||||
// ─── Tool Call Card ───────────────────────────────────────────────────────────
|
||||
|
||||
function ToolCallCard({ step, index }: { step: ToolCallStep; index: number }) {
|
||||
function ToolCallCard({ step, index }: { step: any; index: number }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const argsSummary = () => {
|
||||
@@ -151,7 +176,6 @@ function ToolCallCard({ step, index }: { step: ToolCallStep; index: number }) {
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="px-3 pb-3 space-y-2 border-t border-border/30">
|
||||
<div>
|
||||
@@ -192,7 +216,6 @@ function MessageBubble({ msg }: { msg: ChatMessage }) {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className={`flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"}`}
|
||||
>
|
||||
{/* Avatar */}
|
||||
<div
|
||||
className={`shrink-0 w-7 h-7 rounded-md flex items-center justify-center border ${
|
||||
isUser
|
||||
@@ -210,12 +233,9 @@ function MessageBubble({ msg }: { msg: ChatMessage }) {
|
||||
<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`}
|
||||
>
|
||||
{/* Meta */}
|
||||
<div
|
||||
className={`flex items-center gap-2 ${isUser ? "flex-row-reverse" : ""}`}
|
||||
>
|
||||
@@ -236,8 +256,6 @@ function MessageBubble({ msg }: { msg: ChatMessage }) {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tool calls */}
|
||||
{msg.toolCalls && msg.toolCalls.length > 0 && (
|
||||
<div className="w-full space-y-1 mb-1">
|
||||
<p className="text-[10px] font-mono text-muted-foreground flex items-center gap-1">
|
||||
@@ -250,8 +268,6 @@ function MessageBubble({ msg }: { msg: ChatMessage }) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Message text */}
|
||||
{msg.content && (
|
||||
<div
|
||||
className={`rounded-lg px-3 py-2 max-w-[85%] ${
|
||||
@@ -280,16 +296,76 @@ function MessageBubble({ msg }: { msg: ChatMessage }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Console Panel ────────────────────────────────────────────────────────────
|
||||
// ─── Agent Activity Card ──────────────────────────────────────────────────────
|
||||
|
||||
function ConsolePanel({ messages }: { messages: ChatMessage[] }) {
|
||||
function AgentActivityCard({ activity }: { activity: AgentActivity }) {
|
||||
const elapsed = useElapsedTime(activity.startedAt, activity.status);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="rounded border border-[#00D4FF]/30 bg-[#00D4FF]/5 overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center gap-2 px-2.5 py-2">
|
||||
<Radio
|
||||
className={`w-3 h-3 ${activity.status === "running" ? "text-[#00FF88] animate-pulse" : activity.status === "done" ? "text-[#00FF88]" : "text-[#FF3366]"}`}
|
||||
/>
|
||||
<span className="text-[10px] font-mono font-semibold text-foreground flex-1">
|
||||
{activity.agentName}
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[8px] h-3.5 px-1 font-mono border-[#00D4FF]/30 text-[#00D4FF]/80"
|
||||
>
|
||||
{activity.agentRole}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="px-2.5 pb-2 flex items-center gap-2">
|
||||
{activity.containerId && (
|
||||
<span className="text-[9px] font-mono text-muted-foreground/50 flex items-center gap-1">
|
||||
<Container className="w-2.5 h-2.5" />
|
||||
{activity.containerId.slice(0, 12)}
|
||||
</span>
|
||||
)}
|
||||
{activity.status === "running" && (
|
||||
<span className="text-[9px] font-mono text-[#00FF88]/70 flex items-center gap-1">
|
||||
<Clock className="w-2.5 h-2.5" />
|
||||
{elapsed}
|
||||
</span>
|
||||
)}
|
||||
{activity.status === "running" && (
|
||||
<span className="text-[9px] font-mono text-[#00FF88] flex items-center gap-1">
|
||||
<Activity className="w-2.5 h-2.5 animate-pulse" />
|
||||
ping...
|
||||
</span>
|
||||
)}
|
||||
{activity.status === "done" && (
|
||||
<span className="text-[9px] font-mono text-[#00FF88]">completed</span>
|
||||
)}
|
||||
{activity.status === "error" && (
|
||||
<span className="text-[9px] font-mono text-[#FF3366]">error</span>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Console Panel ─────────────────────────────────────────────────────────────
|
||||
|
||||
function ConsolePanel({
|
||||
messages,
|
||||
activeAgents,
|
||||
}: {
|
||||
messages: ChatMessage[];
|
||||
activeAgents: AgentActivity[];
|
||||
}) {
|
||||
const consoleRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (consoleRef.current) {
|
||||
if (consoleRef.current)
|
||||
consoleRef.current.scrollTop = consoleRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages]);
|
||||
}, [messages, activeAgents]);
|
||||
|
||||
const consoleEntries = messages.filter(
|
||||
m => m.toolCalls && m.toolCalls.length > 0
|
||||
@@ -303,34 +379,39 @@ function ConsolePanel({ messages }: { messages: ChatMessage[] }) {
|
||||
<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>
|
||||
{activeAgents.filter(a => a.status === "running").length > 0 && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[8px] h-3.5 px-1 font-mono border-[#00FF88]/30 text-[#00FF88] animate-pulse"
|
||||
>
|
||||
{activeAgents.filter(a => a.status === "running").length} active
|
||||
</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 ref={consoleRef} className="flex-1 overflow-y-auto p-2 space-y-1.5">
|
||||
{/* Active agents section */}
|
||||
{activeAgents.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<p className="text-[8px] font-mono text-[#00D4FF]/50 uppercase tracking-wider mb-1.5 px-1">
|
||||
Active Agents
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{activeAgents.map(a => (
|
||||
<AgentActivityCard key={a.id} activity={a} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool output */}
|
||||
{consoleEntries.length === 0 && activeAgents.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">
|
||||
<p className="text-[10px] font-mono 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 =>
|
||||
@@ -341,13 +422,13 @@ function ConsolePanel({ messages }: { messages: ChatMessage[] }) {
|
||||
>
|
||||
<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">
|
||||
<span className="text-[#00D4FF]/80 font-medium text-[10px] font-mono">
|
||||
{toolLabel(step.tool)}
|
||||
</span>
|
||||
<span className="text-muted-foreground/50 flex-1 truncate">
|
||||
<span className="text-muted-foreground/50 flex-1 truncate text-[9px] font-mono">
|
||||
{step.args.command || step.args.path || ""}
|
||||
</span>
|
||||
<span className="text-muted-foreground/40">
|
||||
<span className="text-muted-foreground/40 text-[9px] font-mono">
|
||||
{step.durationMs}ms
|
||||
</span>
|
||||
{step.success ? (
|
||||
@@ -356,7 +437,7 @@ function ConsolePanel({ messages }: { messages: ChatMessage[] }) {
|
||||
<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">
|
||||
<pre className="px-2 py-1.5 text-foreground/60 bg-background/30 max-h-20 overflow-auto whitespace-pre-wrap break-words text-[9px] font-mono">
|
||||
{step.success
|
||||
? typeof step.result === "string"
|
||||
? step.result.slice(0, 500)
|
||||
@@ -372,7 +453,7 @@ function ConsolePanel({ messages }: { messages: ChatMessage[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Sidebar Tab Button ──────────────────────────────────────────────────────
|
||||
// ─── Sidebar Tab Button ───────────────────────────────────────────────────────
|
||||
|
||||
function SidebarTabButton({
|
||||
active,
|
||||
@@ -400,11 +481,7 @@ function SidebarTabButton({
|
||||
{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"
|
||||
}`}
|
||||
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>
|
||||
@@ -413,21 +490,82 @@ function SidebarTabButton({
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Conversation List Item ────────────────────────────────────────────────────
|
||||
|
||||
function ConversationItem({
|
||||
conv,
|
||||
isActive,
|
||||
onClick,
|
||||
onDelete,
|
||||
}: {
|
||||
conv: Conversation;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const lastMsg = conv.messages[conv.messages.length - 1];
|
||||
const lastContent =
|
||||
lastMsg?.role === "user"
|
||||
? lastMsg.content
|
||||
: lastMsg?.role === "assistant"
|
||||
? lastMsg.content.slice(0, 30)
|
||||
: "";
|
||||
const msgCount = conv.messages.filter(m => m.role !== "system").length;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
<MessageSquare
|
||||
className={`w-3 h-3 mt-0.5 shrink-0 ${isActive ? "text-[#00D4FF]" : "text-muted-foreground/40"}`}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p
|
||||
className={`text-[10px] font-mono truncate ${isActive ? "text-foreground" : "text-foreground/70"}`}
|
||||
>
|
||||
{conv.title}
|
||||
</p>
|
||||
{lastContent && (
|
||||
<p className="text-[8px] font-mono text-muted-foreground/40 truncate">
|
||||
{lastContent}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
<span className="text-[8px] font-mono text-muted-foreground/30">
|
||||
{msgCount} msg
|
||||
</span>
|
||||
<span className="text-[8px] font-mono text-muted-foreground/20">
|
||||
{new Date(conv.createdAt).toLocaleDateString("ru-RU", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
className="opacity-0 group-hover:opacity-100 p-0.5 text-muted-foreground/30 hover:text-[#FF3366] transition-all shrink-0"
|
||||
>
|
||||
<Trash2 className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main Chat Component ──────────────────────────────────────────────────────
|
||||
|
||||
export default function Chat() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [conversationHistory, setConversationHistory] = useState<
|
||||
Array<{ role: "user" | "assistant" | "system"; content: string }>
|
||||
>([]);
|
||||
const { conversations, activeId, active, isThinking, activeAgents } =
|
||||
useChatStore();
|
||||
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 [conversationId] = useState(`conv-${Date.now()}`);
|
||||
const [activeTab, setActiveTab] = useState<SidebarTab>("console");
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -437,29 +575,22 @@ export default function Chat() {
|
||||
});
|
||||
const orchestratorMutation = trpc.orchestrator.chat.useMutation();
|
||||
const orchestratorConfigQuery = trpc.orchestrator.getConfig.useQuery();
|
||||
const researchMutation = trpc.research.search.useMutation();
|
||||
|
||||
// Initialize first conversation if empty
|
||||
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",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
if (orchestratorConfigQuery.data && conversations.length === 0) {
|
||||
chatStore.createConversation(
|
||||
orchestratorConfigQuery.data.name ?? "GoClaw Orchestrator"
|
||||
);
|
||||
}
|
||||
}, [orchestratorConfigQuery.data]);
|
||||
}, [orchestratorConfigQuery.data, conversations.length]);
|
||||
|
||||
// Auto-scroll
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
if (scrollRef.current)
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages]);
|
||||
}, [active?.messages]);
|
||||
|
||||
// Auto-resize textarea
|
||||
const adjustTextareaHeight = useCallback(() => {
|
||||
@@ -473,124 +604,86 @@ export default function Chat() {
|
||||
adjustTextareaHeight();
|
||||
}, [input, adjustTextareaHeight]);
|
||||
|
||||
const getTs = () =>
|
||||
new Date().toLocaleTimeString("ru-RU", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
const messages = active?.messages ?? [];
|
||||
const convId = activeId;
|
||||
|
||||
// Track research activity
|
||||
useEffect(() => {
|
||||
if (researchMutation.isPending && activeId) {
|
||||
const activityId = chatStore.startAgentActivity(
|
||||
"Researcher",
|
||||
"browser",
|
||||
"goclaw-agent-1"
|
||||
);
|
||||
return () => {
|
||||
if (researchMutation.isSuccess) {
|
||||
chatStore.updateAgentActivity(activityId, {
|
||||
status: "done",
|
||||
result: "Research completed",
|
||||
});
|
||||
setTimeout(() => chatStore.removeAgentActivity(activityId), 3000);
|
||||
} else if (researchMutation.isError) {
|
||||
chatStore.updateAgentActivity(activityId, {
|
||||
status: "error",
|
||||
error: "Research failed",
|
||||
});
|
||||
setTimeout(() => chatStore.removeAgentActivity(activityId), 5000);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [
|
||||
researchMutation.isPending,
|
||||
researchMutation.isSuccess,
|
||||
researchMutation.isError,
|
||||
activeId,
|
||||
]);
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!input.trim() || isThinking) return;
|
||||
|
||||
const userContent = input.trim();
|
||||
const ts = getTs();
|
||||
|
||||
const userMsg: ChatMessage = {
|
||||
id: `user-${Date.now()}`,
|
||||
role: "user",
|
||||
content: userContent,
|
||||
timestamp: ts,
|
||||
};
|
||||
setMessages(prev => [...prev, userMsg]);
|
||||
|
||||
const newHistory = [
|
||||
...conversationHistory,
|
||||
{ role: "user" as const, content: userContent },
|
||||
];
|
||||
setConversationHistory(newHistory);
|
||||
const { convId: cid, newHistory } = chatStore.addUserMessage(
|
||||
input.trim(),
|
||||
convId
|
||||
);
|
||||
setInput("");
|
||||
setIsThinking(true);
|
||||
|
||||
// Switch to console tab when sending a message
|
||||
chatStore.setThinking(true);
|
||||
setActiveTab("console");
|
||||
|
||||
const thinkingId = `thinking-${Date.now()}`;
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{
|
||||
id: thinkingId,
|
||||
role: "system" as const,
|
||||
content: "Orchestrator is processing...",
|
||||
timestamp: getTs(),
|
||||
},
|
||||
]);
|
||||
const thinkingId = chatStore.addThinkingMessage(cid);
|
||||
|
||||
try {
|
||||
const result = await orchestratorMutation.mutateAsync({
|
||||
messages: newHistory,
|
||||
maxIterations: 10,
|
||||
});
|
||||
|
||||
setMessages(prev => prev.filter(m => m.id !== thinkingId));
|
||||
|
||||
const respTs = getTs();
|
||||
|
||||
setLastError(null);
|
||||
setRetryAttempt(0);
|
||||
chatStore.removeThinkingMessage(cid, thinkingId);
|
||||
|
||||
if (result.success) {
|
||||
setConversationHistory(prev => [
|
||||
...prev,
|
||||
const updatedHistory = [
|
||||
...newHistory,
|
||||
{ role: "assistant" as const, content: result.response },
|
||||
]);
|
||||
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{
|
||||
id: `resp-${Date.now()}`,
|
||||
role: "assistant" as const,
|
||||
content: result.response,
|
||||
timestamp: respTs,
|
||||
toolCalls: result.toolCalls,
|
||||
model: result.model,
|
||||
usage: result.usage,
|
||||
},
|
||||
]);
|
||||
];
|
||||
chatStore.addAssistantMessage(
|
||||
cid,
|
||||
result.response,
|
||||
result.toolCalls,
|
||||
result.model,
|
||||
result.usage,
|
||||
updatedHistory
|
||||
);
|
||||
} else {
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{
|
||||
id: `err-${Date.now()}`,
|
||||
role: "assistant" as const,
|
||||
content: `Error: ${result.error || "Unknown error"}`,
|
||||
timestamp: respTs,
|
||||
isError: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
} catch (err: any) {
|
||||
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");
|
||||
|
||||
setLastError({ message: errorMsg, isRetryable });
|
||||
setRetryAttempt(prev => prev + 1);
|
||||
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{
|
||||
id: `err-${Date.now()}`,
|
||||
role: "assistant" as const,
|
||||
content: `Network Error (Attempt ${retryAttempt + 1}): ${errorMsg}${isRetryable ? "\n\nRetrying automatically..." : ""}`,
|
||||
timestamp: getTs(),
|
||||
isError: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (isRetryable && retryAttempt < 2) {
|
||||
setTimeout(
|
||||
() => {
|
||||
sendMessage();
|
||||
},
|
||||
1000 * Math.pow(2, retryAttempt)
|
||||
chatStore.addErrorMessage(
|
||||
cid,
|
||||
`Error: ${result.error || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
chatStore.removeThinkingMessage(cid, thinkingId);
|
||||
chatStore.addErrorMessage(
|
||||
cid,
|
||||
`Network Error: ${err.message || "Unknown error"}`
|
||||
);
|
||||
} finally {
|
||||
setIsThinking(false);
|
||||
setTimeout(() => textareaRef.current?.focus(), 100);
|
||||
}
|
||||
};
|
||||
@@ -603,20 +696,22 @@ export default function Chat() {
|
||||
};
|
||||
|
||||
const agents = agentsQuery.data ?? [];
|
||||
const activeAgents = agents.filter(
|
||||
a => a.isActive && !(a as any).isOrchestrator
|
||||
const activeAgentsList = agents.filter(
|
||||
(a: any) => a.isActive && !a.isOrchestrator
|
||||
);
|
||||
const orchConfig = orchestratorConfigQuery.data;
|
||||
|
||||
const toolCallCount = messages.reduce(
|
||||
(acc, m) => acc + (m.toolCalls?.length ?? 0),
|
||||
0
|
||||
);
|
||||
const runningAgentCount = activeAgents.filter(
|
||||
a => a.status === "running"
|
||||
).length;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-3">
|
||||
<div className="h-full flex flex-col gap-0 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between shrink-0">
|
||||
<div className="flex items-center justify-between shrink-0 px-1 pb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<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]" />
|
||||
@@ -627,22 +722,26 @@ export default function Chat() {
|
||||
</h2>
|
||||
<p className="text-[11px] font-mono text-muted-foreground">
|
||||
{orchConfig ? (
|
||||
<span>
|
||||
<>
|
||||
<span className="text-[#00D4FF]/80">{orchConfig.model}</span>
|
||||
{" · "}
|
||||
{activeAgents.length} agents · {ORCHESTRATOR_TOOLS_COUNT}{" "}
|
||||
{activeAgentsList.length} agents · {ORCHESTRATOR_TOOLS_COUNT}{" "}
|
||||
tools
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
`Main AI · ${activeAgents.length} agents · ${ORCHESTRATOR_TOOLS_COUNT} tools`
|
||||
`Main AI · ${activeAgentsList.length} agents · ${ORCHESTRATOR_TOOLS_COUNT} tools`
|
||||
)}
|
||||
{runningAgentCount > 0 && (
|
||||
<span className="ml-2 text-[#00FF88]">
|
||||
● {runningAgentCount} running
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 => (
|
||||
{activeAgentsList.slice(0, 3).map((agent: any) => (
|
||||
<Badge
|
||||
key={agent.id}
|
||||
variant="outline"
|
||||
@@ -670,10 +769,44 @@ export default function Chat() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex-1 flex gap-3 min-h-0">
|
||||
{/* Chat area */}
|
||||
<Card className="flex-1 bg-card border-border/50 overflow-hidden">
|
||||
{/* Main 3-panel layout */}
|
||||
<div className="flex-1 flex gap-2 min-h-0">
|
||||
{/* ─── Left Panel — Conversations ─── */}
|
||||
<div className="w-48 shrink-0 flex flex-col gap-2 min-h-0">
|
||||
<div className="flex items-center justify-between shrink-0">
|
||||
<span className="text-[10px] font-mono text-muted-foreground/60 uppercase tracking-wider">
|
||||
Chats
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
chatStore.createConversation(
|
||||
orchConfig?.name ?? "GoClaw Orchestrator"
|
||||
)
|
||||
}
|
||||
className="h-5 w-5 p-0 text-muted-foreground/40 hover:text-[#00D4FF]"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea className="flex-1 min-h-0">
|
||||
<div className="space-y-0.5 pr-0.5">
|
||||
{conversations.map(c => (
|
||||
<ConversationItem
|
||||
key={c.id}
|
||||
conv={c}
|
||||
isActive={c.id === activeId}
|
||||
onClick={() => chatStore.setActiveId(c.id)}
|
||||
onDelete={() => chatStore.deleteConversation(c.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* ─── 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">
|
||||
@@ -682,7 +815,6 @@ export default function Chat() {
|
||||
<MessageBubble key={msg.id} msg={msg} />
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{isThinking && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
@@ -693,6 +825,12 @@ export default function Chat() {
|
||||
<span className="text-muted-foreground">
|
||||
Orchestrator thinking...
|
||||
</span>
|
||||
{runningAgentCount > 0 && (
|
||||
<span className="text-[#00FF88] text-[10px]">
|
||||
({runningAgentCount} agent
|
||||
{runningAgentCount > 1 ? "s" : ""} active)
|
||||
</span>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
@@ -700,7 +838,6 @@ export default function Chat() {
|
||||
|
||||
{/* 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">
|
||||
{[
|
||||
"Список агентов",
|
||||
@@ -718,7 +855,6 @@ export default function Chat() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-end gap-2">
|
||||
<span className="text-[#00D4FF] font-mono text-sm shrink-0 pb-1">
|
||||
$
|
||||
@@ -730,8 +866,8 @@ export default function Chat() {
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
isThinking
|
||||
? "Ожидание ответа оркестратора..."
|
||||
: "Введите команду или вопрос... (Shift+Enter для новой строки)"
|
||||
? "Ожидание ответа..."
|
||||
: "Введите команду... (Shift+Enter для новой строки)"
|
||||
}
|
||||
disabled={isThinking}
|
||||
rows={1}
|
||||
@@ -754,16 +890,15 @@ export default function Chat() {
|
||||
</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 */}
|
||||
{/* ─── Right Sidebar — Tabbed Panel ─── */}
|
||||
<div className="w-80 border border-border/30 bg-card rounded-lg overflow-hidden flex flex-col shrink-0">
|
||||
<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}
|
||||
count={toolCallCount + runningAgentCount}
|
||||
/>
|
||||
<SidebarTabButton
|
||||
active={activeTab === "tasks"}
|
||||
@@ -778,15 +913,13 @@ export default function Chat() {
|
||||
label="Research"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="flex-1 min-h-0">
|
||||
{activeTab === "console" && <ConsolePanel messages={messages} />}
|
||||
{activeTab === "tasks" && (
|
||||
<TasksPanel conversationId={conversationId} />
|
||||
{activeTab === "console" && (
|
||||
<ConsolePanel messages={messages} activeAgents={activeAgents} />
|
||||
)}
|
||||
{activeTab === "tasks" && <TasksPanel conversationId={convId} />}
|
||||
{activeTab === "research" && (
|
||||
<WebResearchPanel conversationId={conversationId} />
|
||||
<WebResearchPanel conversationId={convId ?? ""} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user