Files
stack/apps/web/src/components/hud/HUD.tsx
Jason Woltje d34f097a5c
All checks were successful
ci/woodpecker/push/web Pipeline was successful
feat(web): add orchestrator events widget with matrix signal visibility
2026-02-17 15:56:12 -06:00

190 lines
5.5 KiB
TypeScript

/**
* HUD container - main dashboard interface
*/
import { useMemo } from "react";
import { Button } from "@mosaic/ui";
import { RotateCcw } from "lucide-react";
import { WidgetGrid } from "./WidgetGrid";
import { WidgetRenderer } from "./WidgetRenderer";
import { useLayout } from "@/lib/hooks/useLayout";
import type { WidgetPlacement } from "@mosaic/shared";
export interface HUDProps {
className?: string;
}
/**
* Registry of available widget components
* This will be populated with actual widget components
*/
const WIDGET_REGISTRY = {
TasksWidget: {
name: "tasks",
displayName: "Tasks",
description: "View and manage your tasks",
defaultWidth: 2,
defaultHeight: 3,
minWidth: 1,
minHeight: 2,
},
CalendarWidget: {
name: "calendar",
displayName: "Calendar",
description: "Upcoming events and schedule",
defaultWidth: 2,
defaultHeight: 2,
minWidth: 1,
minHeight: 2,
},
QuickCaptureWidget: {
name: "quick-capture",
displayName: "Quick Capture",
description: "Capture ideas and notes",
defaultWidth: 2,
defaultHeight: 1,
minWidth: 1,
minHeight: 1,
},
AgentStatusWidget: {
name: "agent-status",
displayName: "Agent Status",
description: "View running agent sessions",
defaultWidth: 2,
defaultHeight: 2,
minWidth: 1,
minHeight: 1,
},
OrchestratorEventsWidget: {
name: "orchestrator-events",
displayName: "Orchestrator Events",
description: "Recent events and stream health for orchestration",
defaultWidth: 2,
defaultHeight: 2,
minWidth: 1,
minHeight: 1,
},
} as const;
type WidgetRegistryKey = keyof typeof WIDGET_REGISTRY;
export function HUD({ className = "" }: HUDProps): React.JSX.Element {
const { currentLayout, updateLayout, addWidget, removeWidget, switchLayout, resetLayout } =
useLayout();
const isEditing = true; // For now, always in edit mode (can be toggled later)
const handleLayoutChange = (
newLayout: readonly { i: string; x: number; y: number; w: number; h: number }[]
): void => {
updateLayout([...newLayout] as WidgetPlacement[]);
};
const handleAddWidget = (widgetType: WidgetRegistryKey): void => {
const widgetConfig = WIDGET_REGISTRY[widgetType];
const widgetId = `${widgetConfig.name}-${String(Date.now())}`;
// Find the next available position
const maxY = currentLayout?.layout.reduce((max, w): number => Math.max(max, w.y + w.h), 0) ?? 0;
const newWidget = {
i: widgetId,
x: 0,
y: maxY,
w: widgetConfig.defaultWidth,
h: widgetConfig.defaultHeight,
minW: widgetConfig.minWidth,
minH: widgetConfig.minHeight,
isDraggable: true,
isResizable: true,
};
addWidget(newWidget);
};
const handleResetLayout = (): void => {
if (confirm("Are you sure you want to reset the layout? This will remove all widgets.")) {
resetLayout();
}
};
const widgetComponents = useMemo(() => {
if (!currentLayout?.layout) return [];
return currentLayout.layout.map((widget) => (
<WidgetRenderer
key={widget.i}
widget={widget}
isEditing={isEditing}
onRemove={removeWidget}
/>
));
}, [currentLayout?.layout, isEditing, removeWidget]);
return (
<div className={`hud-container ${className}`}>
{/* Toolbar */}
<div className="bg-white border-b border-gray-200 px-6 py-4 mb-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
<div className="flex items-center gap-2">
<select
value={currentLayout?.id ?? ""}
onChange={(e): void => {
switchLayout(e.target.value);
}}
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Default Layout</option>
{/* Add more layout options here */}
</select>
<Button
onClick={handleResetLayout}
variant="secondary"
size="sm"
className="flex items-center gap-2"
>
<RotateCcw className="w-4 h-4" />
Reset
</Button>
{/* Widget type selector */}
<div className="relative">
<select
onChange={(e): void => {
const widgetType = e.target.value;
if (widgetType && widgetType in WIDGET_REGISTRY) {
handleAddWidget(widgetType as WidgetRegistryKey);
e.target.value = "";
}
}}
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer"
defaultValue=""
>
<option value="" disabled>
Add Widget
</option>
{Object.entries(WIDGET_REGISTRY).map(([key, config]) => (
<option key={key} value={key}>
{config.displayName}
</option>
))}
</select>
</div>
</div>
</div>
</div>
{/* Widget Grid */}
<WidgetGrid
layout={currentLayout?.layout ?? []}
onLayoutChange={handleLayoutChange}
isEditing={isEditing}
>
{widgetComponents}
</WidgetGrid>
</div>
);
}