From 4500f80e4c0c3f702d4671ff3d0a73faf181f32f Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 22 Feb 2026 15:17:32 -0600 Subject: [PATCH] =?UTF-8?q?feat(web):=20Phase=203=20=E2=80=94=20Dashboard?= =?UTF-8?q?=20page=20with=20reference=20design=20layout=20(MS15-DASH-001?= =?UTF-8?q?=20through=20DASH-005)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/web/src/app/(authenticated)/page.tsx | 92 ++----- .../src/components/dashboard/ActivityFeed.tsx | 169 ++++++++++++ .../components/dashboard/DashboardMetrics.tsx | 45 ++++ .../dashboard/OrchestratorSessions.tsx | 241 ++++++++++++++++++ .../src/components/dashboard/QuickActions.tsx | 96 +++++++ .../src/components/dashboard/TokenBudget.tsx | 98 +++++++ 6 files changed, 672 insertions(+), 69 deletions(-) create mode 100644 apps/web/src/components/dashboard/ActivityFeed.tsx create mode 100644 apps/web/src/components/dashboard/DashboardMetrics.tsx create mode 100644 apps/web/src/components/dashboard/OrchestratorSessions.tsx create mode 100644 apps/web/src/components/dashboard/QuickActions.tsx create mode 100644 apps/web/src/components/dashboard/TokenBudget.tsx diff --git a/apps/web/src/app/(authenticated)/page.tsx b/apps/web/src/app/(authenticated)/page.tsx index 8d637f7..85ace31 100644 --- a/apps/web/src/app/(authenticated)/page.tsx +++ b/apps/web/src/app/(authenticated)/page.tsx @@ -1,78 +1,32 @@ "use client"; -import { useState, useEffect } from "react"; import type { ReactElement } from "react"; -import { RecentTasksWidget } from "@/components/dashboard/RecentTasksWidget"; -import { UpcomingEventsWidget } from "@/components/dashboard/UpcomingEventsWidget"; -import { QuickCaptureWidget } from "@/components/dashboard/QuickCaptureWidget"; -import { DomainOverviewWidget } from "@/components/dashboard/DomainOverviewWidget"; -import { mockTasks } from "@/lib/api/tasks"; -import { mockEvents } from "@/lib/api/events"; -import type { Task, Event } from "@mosaic/shared"; +import { DashboardMetrics } from "@/components/dashboard/DashboardMetrics"; +import { OrchestratorSessions } from "@/components/dashboard/OrchestratorSessions"; +import { QuickActions } from "@/components/dashboard/QuickActions"; +import { ActivityFeed } from "@/components/dashboard/ActivityFeed"; +import { TokenBudget } from "@/components/dashboard/TokenBudget"; export default function DashboardPage(): ReactElement { - const [tasks, setTasks] = useState([]); - const [events, setEvents] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - void loadDashboardData(); - }, []); - - async function loadDashboardData(): Promise { - 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 ( -
-
-

Dashboard

-

Welcome back! Here's your overview

+
+ +
+
+ + +
+
+ + +
- - {error !== null ? ( -
-

{error}

- -
- ) : ( -
- {/* Top row: Domain Overview and Quick Capture */} -
- -
- - - - -
- -
-
- )} -
+ ); } diff --git a/apps/web/src/components/dashboard/ActivityFeed.tsx b/apps/web/src/components/dashboard/ActivityFeed.tsx new file mode 100644 index 0000000..97389cb --- /dev/null +++ b/apps/web/src/components/dashboard/ActivityFeed.tsx @@ -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 ( +
+
+ {item.icon} +
+
+
+ {item.title} + {item.highlight} + {item.rest} + {item.badge !== undefined && ( + + {item.badge.text} + + )} +
+
+ {item.timestamp} +
+
+
+ ); +} + +export function ActivityFeed(): ReactElement { + return ( + + +
+ {activityItems.map((item) => ( + + ))} +
+
+ ); +} diff --git a/apps/web/src/components/dashboard/DashboardMetrics.tsx b/apps/web/src/components/dashboard/DashboardMetrics.tsx new file mode 100644 index 0000000..16a5945 --- /dev/null +++ b/apps/web/src/components/dashboard/DashboardMetrics.tsx @@ -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 ; +} diff --git a/apps/web/src/components/dashboard/OrchestratorSessions.tsx b/apps/web/src/components/dashboard/OrchestratorSessions.tsx new file mode 100644 index 0000000..d496470 --- /dev/null +++ b/apps/web/src/components/dashboard/OrchestratorSessions.tsx @@ -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 ( +
{ + 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", + }} + > +
+ {agent.initials} +
+
+
+ {agent.name} +
+
+ {agent.task} +
+
+ +
+ ); +} + +interface OrchCardProps { + session: OrchestratorSession; +} + +function OrchCard({ session }: OrchCardProps): ReactElement { + return ( +
+
+ + + {session.orchId} + + + {session.name} + + {session.badge} + + {session.duration} + +
+
+ {session.agents.map((agent) => ( + + ))} +
+
+ ); +} + +export function OrchestratorSessions(): ReactElement { + return ( + + 3 active} + /> +
+ {sessions.map((session) => ( + + ))} +
+
+ ); +} diff --git a/apps/web/src/components/dashboard/QuickActions.tsx b/apps/web/src/components/dashboard/QuickActions.tsx new file mode 100644 index 0000000..0bf4f08 --- /dev/null +++ b/apps/web/src/components/dashboard/QuickActions.tsx @@ -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 ( + + ); +} + +export function QuickActions(): ReactElement { + return ( + + +
+ {actions.map((action) => ( + + ))} +
+
+ ); +} diff --git a/apps/web/src/components/dashboard/TokenBudget.tsx b/apps/web/src/components/dashboard/TokenBudget.tsx new file mode 100644 index 0000000..7aad59d --- /dev/null +++ b/apps/web/src/components/dashboard/TokenBudget.tsx @@ -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 ( +
+
+ + {model.label} + + + {model.usage} + +
+ +
+ ); +} + +export function TokenBudget(): ReactElement { + return ( + + +
+ {models.map((model) => ( + + ))} +
+
+ ); +}