Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions tinyoffice/src/app/(office)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function OfficeLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
type TeamConfig,
} from "@/lib/api";
import { Users, Bot, Crown } from "lucide-react";
import { useRouter } from "next/navigation";

// ── Custom Nodes ──────────────────────────────────────────────────────────

Expand Down Expand Up @@ -127,16 +126,13 @@ function buildOrgChart(
const teamEntries = Object.entries(teams);
const allAgentIds = Object.keys(agents);

// Find agents assigned to at least one team
const assignedAgentIds = new Set<string>();
for (const [, team] of teamEntries) {
for (const aid of team.agents) assignedAgentIds.add(aid);
}

// Unassigned agents
const unassignedAgentIds = allAgentIds.filter((id) => !assignedAgentIds.has(id));

// Calculate groups: each team is a group, plus unassigned group
const groups: {
headerId: string;
headerType: string;
Expand All @@ -162,7 +158,6 @@ function buildOrgChart(
label: agents[aid].name,
model: `${agents[aid].provider}/${agents[aid].model}`,
}));
// Sort so leader comes first
members.sort((a, b) => (b.isLeader ? 1 : 0) - (a.isLeader ? 1 : 0));

groups.push({
Expand Down Expand Up @@ -191,13 +186,11 @@ function buildOrgChart(
});
}

// Position groups side by side
let groupX = 0;

for (const group of groups) {
const groupWidth = Math.max(1, group.members.length) * (NODE_W + H_GAP) - H_GAP;

// Header node centered above members
const headerX = groupX + groupWidth / 2 - NODE_W / 2;
nodes.push({
id: group.headerId,
Expand All @@ -210,7 +203,6 @@ function buildOrgChart(
},
});

// Member nodes
group.members.forEach((member, i) => {
const memberX = groupX + i * (NODE_W + H_GAP);
const memberY = V_GAP + NODE_H;
Expand Down Expand Up @@ -249,7 +241,6 @@ function OrgChartInner() {
const { data: agents } = usePolling<Record<string, AgentConfig>>(getAgents, 0);
const { data: teams } = usePolling<Record<string, TeamConfig>>(getTeams, 0);
const { fitView } = useReactFlow();
const router = useRouter();

const { nodes, edges } = useMemo(() => {
if (!agents) return { nodes: [], edges: [] };
Expand All @@ -259,18 +250,15 @@ function OrgChartInner() {
const onNodeClick = useCallback(
(_: React.MouseEvent, node: Node) => {
if (node.type === "agent") {
router.push(`/agents/${node.data.agentId}`);
// Could open agent detail panel in the future
} else if (node.type === "team" && node.data.teamId) {
router.push(`/chat/team/${node.data.teamId}`);
// Could navigate to team chat
}
},
[router]
[]
);

// Fit view when data changes
const onNodesChange = useCallback(() => {
// Let ReactFlow handle internal changes
}, []);
const onNodesChange = useCallback(() => {}, []);

useEffect(() => {
if (nodes.length === 0) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useState } from "react";
import { PixelOfficeScene, PIXEL_SCENE_LAYOUT } from "@/components/office/pixel-office-scene";
import { ArchivePanel, type ArchivePanelId } from "@/components/office/archive-panel";
import { ConversationPanel } from "@/components/office/conversation-panel";
import { AgentDetailPanel } from "@/components/office/agent-detail-panel";
import { OverlayBubbles } from "@/components/office/overlay-bubbles";
import { ARCHIVE_BUTTONS } from "@/components/office/types";
import { useOfficeData } from "@/components/office/use-office-data";
Expand All @@ -17,6 +18,11 @@ export default function OfficePage() {
const scene = useSceneLayout({ ...data, ...sse });

const [archivePanel, setArchivePanel] = useState<ArchivePanelId | null>(null);
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);

const handleAgentClick = (agentId: string) => {
setSelectedAgentId((current) => (current === agentId ? null : agentId));
};

return (
<div className="flex h-full flex-col">
Expand All @@ -29,6 +35,7 @@ export default function OfficePage() {
lounge={scene.loungeModel}
taskStations={scene.taskStations}
agents={scene.sceneAgents}
onAgentClick={handleAgentClick}
/>

<div
Expand Down Expand Up @@ -69,12 +76,23 @@ export default function OfficePage() {
/>
)}

<ConversationPanel
agents={data.agents}
agentEntries={scene.agentEntries}
agentHistories={data.agentHistories}
bubbles={sse.bubbles}
/>
{selectedAgentId ? (
<AgentDetailPanel
agentId={selectedAgentId}
agents={data.agents}
agentEntries={scene.agentEntries}
agentHistories={data.agentHistories}
bubbles={sse.bubbles}
onClose={() => setSelectedAgentId(null)}
/>
) : (
<ConversationPanel
agents={data.agents}
agentEntries={scene.agentEntries}
agentHistories={data.agentHistories}
bubbles={sse.bubbles}
/>
)}

<OverlayBubbles bubbles={scene.overlayBubbles} />
</div>
Expand Down
49 changes: 0 additions & 49 deletions tinyoffice/src/app/office/layout.tsx

This file was deleted.

5 changes: 0 additions & 5 deletions tinyoffice/src/app/page.tsx

This file was deleted.

122 changes: 117 additions & 5 deletions tinyoffice/src/components/app-shell.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,126 @@
"use client";

import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation";
import { useTheme } from "next-themes";
import { cn } from "@/lib/utils";
import { Sidebar } from "@/components/sidebar";
import {
Building2,
GitBranch,
ClipboardList,
SlidersHorizontal,
Settings,
Sun,
Moon,
} from "lucide-react";

const tabs = [
{ href: "/", label: "Office", icon: Building2, exact: true },
{ href: "/org-chart", label: "Org Chart", icon: GitBranch },
{ href: "/tasks", label: "Tasks", icon: ClipboardList },
{ href: "/control", label: "Control", icon: SlidersHorizontal },
];

const navLinks = [{ href: "/settings", label: "Settings", icon: Settings }];

export function AppShell({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const { resolvedTheme, setTheme } = useTheme();

// Hide sidebar for routes served by the (office) layout group
const officeRoutes = ["/", "/tasks", "/org-chart", "/control"];
const hideSidebar =
pathname === "/setup" ||
pathname.startsWith("/office") ||
officeRoutes.some((r) =>
r === "/"
? pathname === "/"
: pathname === r || pathname.startsWith(r + "/"),
);

return (
<div className="flex h-screen overflow-hidden">
<Sidebar />
<main className="flex-1 overflow-y-auto">
{children}
</main>
<div className="flex h-screen flex-col overflow-hidden">
{/* Top bar */}
<div className="flex items-center border-b px-4 gap-1 shrink-0">
{/* Logo */}
<Link href="/" className="flex items-center gap-2 pr-4">
<Image
src="/icon.png"
alt="TinyAGI"
width={20}
height={20}
className="h-5 w-5"
/>
<span className="text-sm font-bold tracking-tight">TinyAGI</span>
</Link>

{/* Tabs */}
{tabs.map(({ href, label, icon: Icon, exact }) => {
const active = exact
? pathname === href
: pathname === href || pathname.startsWith(href + "/");
return (
<Link
key={href}
href={href}
className={cn(
"flex items-center gap-1.5 px-3 py-2.5 text-sm font-medium transition-colors",
active
? "text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
<Icon className="h-3.5 w-3.5" />
{label}
</Link>
);
})}

{/* Spacer */}
<div className="flex-1" />

{/* Nav links */}
{navLinks.map(({ href, label, icon: Icon }) => (
<Link
key={href}
href={href}
className={cn(
"flex items-center gap-1.5 px-2.5 py-2.5 text-xs transition-colors",
pathname === href || pathname.startsWith(href + "/")
? "text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
<Icon className="h-3.5 w-3.5" />
{label}
</Link>
))}

{/* Theme toggle */}
<button
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
className="ml-1 p-1.5 text-muted-foreground hover:text-foreground transition-colors"
title={
resolvedTheme === "dark"
? "Switch to light mode"
: "Switch to dark mode"
}
>
{resolvedTheme === "dark" ? (
<Sun className="h-3.5 w-3.5" />
) : (
<Moon className="h-3.5 w-3.5" />
)}
</button>
</div>

{/* Content */}
<div className="flex flex-1 overflow-hidden">
{!hideSidebar && <Sidebar />}
<main className="flex-1 overflow-y-auto">{children}</main>
</div>
</div>
);
}
Loading