feat(web): Phase 3 — Dashboard page with reference design layout (MS15-DASH-001 through DASH-005)
- DashboardMetrics: 6-cell MetricsStrip (agents, tasks, response time, tokens, errors, projects) - OrchestratorSessions: Orch cards with agent nodes, status dots, badges - QuickActions: 2x2 grid (New Project, Spawn Agent, View Telemetry, Review Tasks) - ActivityFeed: 7-item feed with icons, timestamps, warn/error badges - TokenBudget: 4-model progress bars (claude-3-5-sonnet, haiku, gpt-4o, llama-3.3) - Dashboard page: full-width metrics strip + 2-column grid (main + 320px sidebar) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,78 +1,32 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import { RecentTasksWidget } from "@/components/dashboard/RecentTasksWidget";
|
import { DashboardMetrics } from "@/components/dashboard/DashboardMetrics";
|
||||||
import { UpcomingEventsWidget } from "@/components/dashboard/UpcomingEventsWidget";
|
import { OrchestratorSessions } from "@/components/dashboard/OrchestratorSessions";
|
||||||
import { QuickCaptureWidget } from "@/components/dashboard/QuickCaptureWidget";
|
import { QuickActions } from "@/components/dashboard/QuickActions";
|
||||||
import { DomainOverviewWidget } from "@/components/dashboard/DomainOverviewWidget";
|
import { ActivityFeed } from "@/components/dashboard/ActivityFeed";
|
||||||
import { mockTasks } from "@/lib/api/tasks";
|
import { TokenBudget } from "@/components/dashboard/TokenBudget";
|
||||||
import { mockEvents } from "@/lib/api/events";
|
|
||||||
import type { Task, Event } from "@mosaic/shared";
|
|
||||||
|
|
||||||
export default function DashboardPage(): ReactElement {
|
export default function DashboardPage(): ReactElement {
|
||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
|
||||||
const [events, setEvents] = useState<Event[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void loadDashboardData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function loadDashboardData(): Promise<void> {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// TODO: Replace with real API calls when backend is ready
|
|
||||||
// const [tasksData, eventsData] = await Promise.all([fetchTasks(), fetchEvents()]);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
setTasks(mockTasks);
|
|
||||||
setEvents(mockEvents);
|
|
||||||
} catch (err) {
|
|
||||||
setError(
|
|
||||||
err instanceof Error
|
|
||||||
? err.message
|
|
||||||
: "We had trouble loading your dashboard. Please try again when you're ready."
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="container mx-auto px-4 py-8">
|
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||||
<div className="mb-8">
|
<DashboardMetrics />
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
|
<div
|
||||||
<p className="text-gray-600 mt-2">Welcome back! Here's your overview</p>
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 320px",
|
||||||
|
gap: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 16, minWidth: 0 }}>
|
||||||
|
<OrchestratorSessions />
|
||||||
|
<QuickActions />
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||||
|
<ActivityFeed />
|
||||||
|
<TokenBudget />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{error !== null ? (
|
|
||||||
<div className="rounded-lg border border-amber-200 bg-amber-50 p-6 text-center">
|
|
||||||
<p className="text-amber-800">{error}</p>
|
|
||||||
<button
|
|
||||||
onClick={() => void loadDashboardData()}
|
|
||||||
className="mt-4 rounded-md bg-amber-600 px-4 py-2 text-sm font-medium text-white hover:bg-amber-700 transition-colors"
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
{/* Top row: Domain Overview and Quick Capture */}
|
|
||||||
<div className="lg:col-span-2">
|
|
||||||
<DomainOverviewWidget tasks={tasks} isLoading={isLoading} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<RecentTasksWidget tasks={tasks} isLoading={isLoading} />
|
|
||||||
<UpcomingEventsWidget events={events} isLoading={isLoading} />
|
|
||||||
|
|
||||||
<div className="lg:col-span-2">
|
|
||||||
<QuickCaptureWidget />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
169
apps/web/src/components/dashboard/ActivityFeed.tsx
Normal file
169
apps/web/src/components/dashboard/ActivityFeed.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import type { ReactElement } from "react";
|
||||||
|
import { Card, SectionHeader, Badge } from "@mosaic/ui";
|
||||||
|
|
||||||
|
type BadgeVariantType =
|
||||||
|
| "badge-amber"
|
||||||
|
| "badge-red"
|
||||||
|
| "badge-teal"
|
||||||
|
| "badge-blue"
|
||||||
|
| "badge-muted"
|
||||||
|
| "badge-purple"
|
||||||
|
| "badge-pulse";
|
||||||
|
|
||||||
|
interface ActivityItem {
|
||||||
|
id: string;
|
||||||
|
icon: string;
|
||||||
|
iconBg: string;
|
||||||
|
title: string;
|
||||||
|
highlight: string;
|
||||||
|
rest: string;
|
||||||
|
timestamp: string;
|
||||||
|
badge?: {
|
||||||
|
text: string;
|
||||||
|
variant: BadgeVariantType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const activityItems: ActivityItem[] = [
|
||||||
|
{
|
||||||
|
id: "act-1",
|
||||||
|
icon: "✓",
|
||||||
|
iconBg: "rgba(20,184,166,0.15)",
|
||||||
|
title: "",
|
||||||
|
highlight: "planner-agent",
|
||||||
|
rest: " completed task analysis for infra-refactor",
|
||||||
|
timestamp: "2m ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "act-2",
|
||||||
|
icon: "⚠",
|
||||||
|
iconBg: "rgba(245,158,11,0.15)",
|
||||||
|
title: "",
|
||||||
|
highlight: "executor-agent",
|
||||||
|
rest: " hit rate limit on Terraform API",
|
||||||
|
timestamp: "5m ago",
|
||||||
|
badge: { text: "warn", variant: "badge-amber" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "act-3",
|
||||||
|
icon: "↑",
|
||||||
|
iconBg: "rgba(47,128,255,0.15)",
|
||||||
|
title: "",
|
||||||
|
highlight: "ORCH-002",
|
||||||
|
rest: " session started for api-v3-migration",
|
||||||
|
timestamp: "12m ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "act-4",
|
||||||
|
icon: "✗",
|
||||||
|
iconBg: "rgba(229,72,77,0.15)",
|
||||||
|
title: "",
|
||||||
|
highlight: "migrator-agent",
|
||||||
|
rest: " failed to connect to staging database",
|
||||||
|
timestamp: "18m ago",
|
||||||
|
badge: { text: "error", variant: "badge-red" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "act-5",
|
||||||
|
icon: "✓",
|
||||||
|
iconBg: "rgba(20,184,166,0.15)",
|
||||||
|
title: "",
|
||||||
|
highlight: "reviewer-agent",
|
||||||
|
rest: " approved PR #214 in infra-refactor",
|
||||||
|
timestamp: "34m ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "act-6",
|
||||||
|
icon: "⟳",
|
||||||
|
iconBg: "rgba(139,92,246,0.15)",
|
||||||
|
title: "Token budget reset for ",
|
||||||
|
highlight: "gpt-4o",
|
||||||
|
rest: " model",
|
||||||
|
timestamp: "1h ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "act-7",
|
||||||
|
icon: "★",
|
||||||
|
iconBg: "rgba(20,184,166,0.15)",
|
||||||
|
title: "Project ",
|
||||||
|
highlight: "data-pipeline",
|
||||||
|
rest: " marked as completed",
|
||||||
|
timestamp: "2h ago",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ActivityItemRowProps {
|
||||||
|
item: ActivityItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActivityItemRow({ item }: ActivityItemRowProps): ReactElement {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: 10,
|
||||||
|
padding: "8px 0",
|
||||||
|
borderBottom: "1px solid var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
borderRadius: 6,
|
||||||
|
flexShrink: 0,
|
||||||
|
background: item.iconBg,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
color: "var(--text)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
color: "var(--text-2)",
|
||||||
|
lineHeight: 1.4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
<strong style={{ color: "var(--text)" }}>{item.highlight}</strong>
|
||||||
|
{item.rest}
|
||||||
|
{item.badge !== undefined && (
|
||||||
|
<Badge variant={item.badge.variant} style={{ marginLeft: 6 }}>
|
||||||
|
{item.badge.text}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.7rem",
|
||||||
|
fontFamily: "var(--mono)",
|
||||||
|
color: "var(--muted)",
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.timestamp}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ActivityFeed(): ReactElement {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<SectionHeader title="Activity Feed" subtitle="Recent agent events" />
|
||||||
|
<div>
|
||||||
|
{activityItems.map((item) => (
|
||||||
|
<ActivityItemRow key={item.id} item={item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
apps/web/src/components/dashboard/DashboardMetrics.tsx
Normal file
45
apps/web/src/components/dashboard/DashboardMetrics.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { ReactElement } from "react";
|
||||||
|
import { MetricsStrip, type MetricCell } from "@mosaic/ui";
|
||||||
|
|
||||||
|
const cells: MetricCell[] = [
|
||||||
|
{
|
||||||
|
label: "Active Agents",
|
||||||
|
value: "47",
|
||||||
|
color: "var(--ms-blue-400)",
|
||||||
|
trend: { direction: "up", text: "↑ +3 from yesterday" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Tasks Completed",
|
||||||
|
value: "1,284",
|
||||||
|
color: "var(--ms-teal-400)",
|
||||||
|
trend: { direction: "up", text: "↑ +128 today" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Avg Response Time",
|
||||||
|
value: "2.4s",
|
||||||
|
color: "var(--ms-purple-400)",
|
||||||
|
trend: { direction: "down", text: "↓ -0.3s improved" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Token Usage",
|
||||||
|
value: "3.2M",
|
||||||
|
color: "var(--ms-amber-400)",
|
||||||
|
trend: { direction: "neutral", text: "78% of budget" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Error Rate",
|
||||||
|
value: "0.4%",
|
||||||
|
color: "var(--ms-red-400)",
|
||||||
|
trend: { direction: "down", text: "↓ -0.1% improved" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Active Projects",
|
||||||
|
value: "8",
|
||||||
|
color: "var(--ms-cyan-500)",
|
||||||
|
trend: { direction: "neutral", text: "2 deploying" },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function DashboardMetrics(): ReactElement {
|
||||||
|
return <MetricsStrip cells={cells} />;
|
||||||
|
}
|
||||||
241
apps/web/src/components/dashboard/OrchestratorSessions.tsx
Normal file
241
apps/web/src/components/dashboard/OrchestratorSessions.tsx
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import { Card, SectionHeader, Badge, Dot } from "@mosaic/ui";
|
||||||
|
|
||||||
|
interface AgentNode {
|
||||||
|
id: string;
|
||||||
|
initials: string;
|
||||||
|
avatarColor: string;
|
||||||
|
name: string;
|
||||||
|
task: string;
|
||||||
|
status: "teal" | "blue" | "amber" | "red" | "muted";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrchestratorSession {
|
||||||
|
id: string;
|
||||||
|
orchId: string;
|
||||||
|
name: string;
|
||||||
|
badge: string;
|
||||||
|
badgeVariant:
|
||||||
|
| "badge-teal"
|
||||||
|
| "badge-amber"
|
||||||
|
| "badge-red"
|
||||||
|
| "badge-blue"
|
||||||
|
| "badge-muted"
|
||||||
|
| "badge-purple"
|
||||||
|
| "badge-pulse";
|
||||||
|
duration: string;
|
||||||
|
agents: AgentNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessions: OrchestratorSession[] = [
|
||||||
|
{
|
||||||
|
id: "s1",
|
||||||
|
orchId: "ORCH-001",
|
||||||
|
name: "infra-refactor",
|
||||||
|
badge: "running",
|
||||||
|
badgeVariant: "badge-teal",
|
||||||
|
duration: "2h 14m",
|
||||||
|
agents: [
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
initials: "PL",
|
||||||
|
avatarColor: "rgba(47,128,255,0.15)",
|
||||||
|
name: "planner-agent",
|
||||||
|
task: "Analyzing network topology",
|
||||||
|
status: "blue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a2",
|
||||||
|
initials: "EX",
|
||||||
|
avatarColor: "rgba(20,184,166,0.15)",
|
||||||
|
name: "executor-agent",
|
||||||
|
task: "Applying Terraform modules",
|
||||||
|
status: "teal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a3",
|
||||||
|
initials: "QA",
|
||||||
|
avatarColor: "rgba(245,158,11,0.15)",
|
||||||
|
name: "reviewer-agent",
|
||||||
|
task: "Waiting for executor output",
|
||||||
|
status: "amber",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "s2",
|
||||||
|
orchId: "ORCH-002",
|
||||||
|
name: "api-v3-migration",
|
||||||
|
badge: "running",
|
||||||
|
badgeVariant: "badge-teal",
|
||||||
|
duration: "45m",
|
||||||
|
agents: [
|
||||||
|
{
|
||||||
|
id: "a4",
|
||||||
|
initials: "MG",
|
||||||
|
avatarColor: "rgba(139,92,246,0.15)",
|
||||||
|
name: "migrator-agent",
|
||||||
|
task: "Rewriting endpoint handlers",
|
||||||
|
status: "blue",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface AgentNodeItemProps {
|
||||||
|
agent: AgentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AgentNodeItem({ agent }: AgentNodeItemProps): ReactElement {
|
||||||
|
const [hovered, setHovered] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onMouseEnter={(): void => {
|
||||||
|
setHovered(true);
|
||||||
|
}}
|
||||||
|
onMouseLeave={(): void => {
|
||||||
|
setHovered(false);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
padding: "7px 10px",
|
||||||
|
borderRadius: "var(--r-sm)",
|
||||||
|
border: `1px solid ${hovered ? "var(--ms-border-700)" : "var(--border)"}`,
|
||||||
|
background: hovered ? "var(--surface)" : "var(--bg-mid)",
|
||||||
|
transition: "border-color 0.15s, background 0.15s",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
borderRadius: 6,
|
||||||
|
flexShrink: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontWeight: 700,
|
||||||
|
fontFamily: "var(--mono)",
|
||||||
|
fontSize: "0.7rem",
|
||||||
|
background: agent.avatarColor,
|
||||||
|
color: "var(--text)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{agent.initials}
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--text)",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{agent.name}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.72rem",
|
||||||
|
color: "var(--muted)",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{agent.task}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dot variant={agent.status} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrchCardProps {
|
||||||
|
session: OrchestratorSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OrchCard({ session }: OrchCardProps): ReactElement {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "var(--bg-mid)",
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
borderRadius: "var(--r-md)",
|
||||||
|
padding: "12px 14px",
|
||||||
|
marginBottom: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
marginBottom: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dot variant="teal" />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: "var(--mono)",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
color: "var(--ms-purple-400)",
|
||||||
|
fontWeight: 700,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{session.orchId}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--text)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{session.name}
|
||||||
|
</span>
|
||||||
|
<Badge variant={session.badgeVariant}>{session.badge}</Badge>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
marginLeft: "auto",
|
||||||
|
fontSize: "0.72rem",
|
||||||
|
fontFamily: "var(--mono)",
|
||||||
|
color: "var(--muted)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{session.duration}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||||
|
{session.agents.map((agent) => (
|
||||||
|
<AgentNodeItem key={agent.id} agent={agent} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OrchestratorSessions(): ReactElement {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<SectionHeader
|
||||||
|
title="Active Orchestrator Sessions"
|
||||||
|
subtitle="3 of 8 projects running"
|
||||||
|
actions={<Badge variant="badge-teal">3 active</Badge>}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{sessions.map((session) => (
|
||||||
|
<OrchCard key={session.id} session={session} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
96
apps/web/src/components/dashboard/QuickActions.tsx
Normal file
96
apps/web/src/components/dashboard/QuickActions.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import { Card, SectionHeader } from "@mosaic/ui";
|
||||||
|
|
||||||
|
interface QuickAction {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
iconBg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions: QuickAction[] = [
|
||||||
|
{ id: "new-project", label: "New Project", icon: "🚀", iconBg: "rgba(47,128,255,0.15)" },
|
||||||
|
{ id: "spawn-agent", label: "Spawn Agent", icon: "🤖", iconBg: "rgba(139,92,246,0.15)" },
|
||||||
|
{ id: "view-telemetry", label: "View Telemetry", icon: "📊", iconBg: "rgba(20,184,166,0.15)" },
|
||||||
|
{ id: "review-tasks", label: "Review Tasks", icon: "📋", iconBg: "rgba(245,158,11,0.15)" },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ActionButtonProps {
|
||||||
|
action: QuickAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActionButton({ action }: ActionButtonProps): ReactElement {
|
||||||
|
const [hovered, setHovered] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onMouseEnter={(): void => {
|
||||||
|
setHovered(true);
|
||||||
|
}}
|
||||||
|
onMouseLeave={(): void => {
|
||||||
|
setHovered(false);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 8,
|
||||||
|
padding: "16px 12px",
|
||||||
|
borderRadius: "var(--r-md)",
|
||||||
|
border: `1px solid ${hovered ? "var(--ms-border-700)" : "var(--border)"}`,
|
||||||
|
background: hovered ? "var(--surface)" : "var(--bg-mid)",
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "border-color 0.15s, background 0.15s",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 6,
|
||||||
|
background: action.iconBg,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--text)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{action.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QuickActions(): ReactElement {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<SectionHeader title="Quick Actions" />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 1fr",
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{actions.map((action) => (
|
||||||
|
<ActionButton key={action.id} action={action} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
apps/web/src/components/dashboard/TokenBudget.tsx
Normal file
98
apps/web/src/components/dashboard/TokenBudget.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import type { ReactElement } from "react";
|
||||||
|
import { Card, SectionHeader, ProgressBar, type ProgressBarVariant } from "@mosaic/ui";
|
||||||
|
|
||||||
|
interface ModelBudget {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
usage: string;
|
||||||
|
value: number;
|
||||||
|
variant: ProgressBarVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
const models: ModelBudget[] = [
|
||||||
|
{
|
||||||
|
id: "sonnet",
|
||||||
|
label: "claude-3-5-sonnet",
|
||||||
|
usage: "2.1M / 3M",
|
||||||
|
value: 70,
|
||||||
|
variant: "blue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "haiku",
|
||||||
|
label: "claude-3-haiku",
|
||||||
|
usage: "890K / 5M",
|
||||||
|
value: 18,
|
||||||
|
variant: "teal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gpt4o",
|
||||||
|
label: "gpt-4o",
|
||||||
|
usage: "320K / 1M",
|
||||||
|
value: 32,
|
||||||
|
variant: "purple",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "llama",
|
||||||
|
label: "local/llama-3.3",
|
||||||
|
usage: "unlimited",
|
||||||
|
value: 55,
|
||||||
|
variant: "amber",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ModelRowProps {
|
||||||
|
model: ModelBudget;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModelRow({ model }: ModelRowProps): ReactElement {
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: 14 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: 5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--text-2)",
|
||||||
|
fontFamily: "var(--mono)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{model.label}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "0.72rem",
|
||||||
|
color: "var(--muted)",
|
||||||
|
fontFamily: "var(--mono)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{model.usage}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ProgressBar
|
||||||
|
value={model.value}
|
||||||
|
variant={model.variant}
|
||||||
|
label={`${model.label} token usage`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TokenBudget(): ReactElement {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<SectionHeader title="Token Budget" subtitle="Usage by model" />
|
||||||
|
<div>
|
||||||
|
{models.map((model) => (
|
||||||
|
<ModelRow key={model.id} model={model} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user