"use client";
import { useEffect, useState, useCallback } from "react";
import { FileManagerTree } from "./workspace/file-manager-tree";
import { ProfileSwitcher } from "./workspace/profile-switcher";
import { CreateWorkspaceDialog } from "./workspace/create-workspace-dialog";
// --- Types ---
type WebSession = {
id: string;
title: string;
createdAt: number;
updatedAt: number;
messageCount: number;
};
type SkillEntry = {
name: string;
description: string;
emoji?: string;
source: string;
};
type MemoryFile = {
name: string;
sizeBytes: number;
};
type TreeNode = {
name: string;
path: string;
type: "object" | "document" | "folder" | "file" | "database" | "report";
icon?: string;
defaultView?: "table" | "kanban";
children?: TreeNode[];
};
type SidebarSection = "chats" | "skills" | "memories" | "workspace" | "reports";
type SidebarProps = {
onSessionSelect?: (sessionId: string) => void;
onNewSession?: () => void;
activeSessionId?: string;
refreshKey?: number;
};
// --- Helpers ---
function timeAgo(ts: number): string {
const diff = Date.now() - ts;
const seconds = Math.floor(diff / 1000);
if (seconds < 60) {return `${seconds}s ago`;}
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {return `${minutes}m ago`;}
const hours = Math.floor(minutes / 60);
if (hours < 24) {return `${hours}h ago`;}
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
// --- Section Components ---
function ChatsSection({
sessions,
onSessionSelect,
activeSessionId,
}: {
sessions: WebSession[];
onSessionSelect?: (sessionId: string) => void;
activeSessionId?: string;
}) {
const [searchTerm, setSearchTerm] = useState("");
const filteredSessions = sessions.filter((s) =>
s.title.toLowerCase().includes(searchTerm.toLowerCase()),
);
return (
{sessions.length > 3 && (
setSearchTerm(e.target.value)}
placeholder="Search chats..."
className="w-full px-3 py-1.5 text-xs bg-[var(--color-bg)] border border-[var(--color-border)] rounded-md text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:ring-1 focus:ring-[var(--color-accent)] focus:border-transparent"
/>
)}
{filteredSessions.length === 0 ? (
{searchTerm ? "No matching chats." : "No chats yet. Send a message to start."}
) : (
{filteredSessions.map((s) => {
const isActive = s.id === activeSessionId;
return (
onSessionSelect?.(s.id)}
className={`mx-2 px-3 py-2 rounded-lg hover:bg-[var(--color-surface-hover)] cursor-pointer transition-colors ${
isActive
? "bg-[var(--color-surface-hover)] border-l-2 border-[var(--color-accent)]"
: ""
}`}
>
{s.title}
{timeAgo(s.updatedAt)}
{s.messageCount > 0 && (
{s.messageCount} message{s.messageCount !== 1 ? "s" : ""}
)}
);
})}
)}
);
}
function SkillsSection({ skills }: { skills: SkillEntry[] }) {
if (skills.length === 0) {
return No skills found.
;
}
return (
{skills.map((skill) => (
{skill.emoji && {skill.emoji} }
{skill.name}
{skill.source}
{skill.description && (
{skill.description}
)}
))}
);
}
function MemoriesSection({
mainMemory,
dailyLogs,
}: {
mainMemory: string | null;
dailyLogs: MemoryFile[];
}) {
const [expanded, setExpanded] = useState(false);
return (
{mainMemory ? (
setExpanded(!expanded)}
className="text-xs text-[var(--color-accent)] hover:text-[var(--color-accent-hover)] mb-1"
>
{expanded ? "Collapse" : "Show"} MEMORY.md ({mainMemory.length} chars)
{expanded && (
{mainMemory}
)}
) : (
No MEMORY.md found.
)}
{dailyLogs.length > 0 && (
Daily logs ({dailyLogs.length})
{dailyLogs.slice(0, 10).map((log) => (
{log.name}
{(log.sizeBytes / 1024).toFixed(1)}kb
))}
{dailyLogs.length > 10 && (
...and {dailyLogs.length - 10} more
)}
)}
);
}
// --- Workspace Section (uses FileManagerTree in compact mode) ---
function WorkspaceSection({ tree, onRefresh }: { tree: TreeNode[]; onRefresh: () => void }) {
const handleSelect = useCallback((node: TreeNode) => {
// Navigate to workspace page for actionable items
if (node.type === "object" || node.type === "document" || node.type === "file" || node.type === "database" || node.type === "report") {
window.location.href = `/workspace?path=${encodeURIComponent(node.path)}`;
}
}, []);
if (tree.length === 0) {
return (
No workspace data yet.
);
}
return (
);
}
// --- Reports Section ---
function ReportsSection({ tree }: { tree: TreeNode[] }) {
// Collect all report nodes from the tree (recursive)
const reports: TreeNode[] = [];
function collect(nodes: TreeNode[]) {
for (const n of nodes) {
if (n.type === "report") {reports.push(n);}
if (n.children) {collect(n.children);}
}
}
collect(tree);
if (reports.length === 0) {
return (
No reports yet. Ask the agent to create one.
);
}
return (
);
}
// --- Collapsible Header ---
function SectionHeader({
title,
count,
isOpen,
onToggle,
}: {
title: string;
count?: number;
isOpen: boolean;
onToggle: () => void;
}) {
return (
{title}
{count != null && (
({count})
)}
);
}
// --- Main Sidebar ---
export function Sidebar({
onSessionSelect,
onNewSession,
activeSessionId,
refreshKey,
}: SidebarProps) {
const [openSections, setOpenSections] = useState>(new Set(["chats", "workspace"]));
const [webSessions, setWebSessions] = useState([]);
const [skills, setSkills] = useState([]);
const [mainMemory, setMainMemory] = useState(null);
const [dailyLogs, setDailyLogs] = useState([]);
const [workspaceTree, setWorkspaceTree] = useState([]);
const [loading, setLoading] = useState(true);
const [showCreateWorkspace, setShowCreateWorkspace] = useState(false);
const [sidebarRefreshKey, setSidebarRefreshKey] = useState(0);
const toggleSection = (section: SidebarSection) => {
setOpenSections((prev) => {
const next = new Set(prev);
if (next.has(section)) {next.delete(section);}
else {next.add(section);}
return next;
});
};
// Full sidebar re-fetch after profile switch or workspace creation
const handleProfileSwitch = useCallback(() => {
setSidebarRefreshKey((k) => k + 1);
}, []);
// Fetch sidebar data (re-runs when refreshKey or sidebarRefreshKey changes)
useEffect(() => {
async function load() {
setLoading(true);
try {
const [webSessionsRes, skillsRes, memoriesRes, workspaceRes] = await Promise.all([
fetch("/api/web-sessions").then((r) => r.json()),
fetch("/api/skills").then((r) => r.json()),
fetch("/api/memories").then((r) => r.json()),
fetch("/api/workspace/tree").then((r) => r.json()).catch(() => ({ tree: [] })),
]);
setWebSessions(webSessionsRes.sessions ?? []);
setSkills(skillsRes.skills ?? []);
setMainMemory(memoriesRes.mainMemory ?? null);
setDailyLogs(memoriesRes.dailyLogs ?? []);
setWorkspaceTree(workspaceRes.tree ?? []);
} catch (err) {
console.error("Failed to load sidebar data:", err);
} finally {
setLoading(false);
}
}
void load();
}, [refreshKey, sidebarRefreshKey]);
const refreshWorkspace = useCallback(async () => {
try {
const res = await fetch("/api/workspace/tree");
const data = await res.json();
setWorkspaceTree(data.tree ?? []);
} catch {
// ignore
}
}, []);
return (
{/* Header */}
setShowCreateWorkspace(true)}
activeProfileHint={String(sidebarRefreshKey)}
/>
{/* Create workspace dialog */}
setShowCreateWorkspace(false)}
onCreated={handleProfileSwitch}
/>
{/* Content */}
{loading ? (
) : (
<>
{/* Workspace */}
{workspaceTree.length > 0 && (
toggleSection("workspace")}
/>
{openSections.has("workspace") && (
)}
)}
{/* Chats (web sessions) */}
toggleSection("chats")}
/>
{openSections.has("chats") && (
)}
{/* Reports */}
{workspaceTree.length > 0 && (
toggleSection("reports")}
/>
{openSections.has("reports") && (
)}
)}
{/* Skills */}
toggleSection("skills")}
/>
{openSections.has("skills") && }
{/* Memories */}
toggleSection("memories")}
/>
{openSections.has("memories") && (
)}
>
)}
);
}