/** * MiniClaw Dashboard Component * * A self-contained React component for interacting with a MiniClaw server. * This file can be dropped into any React 18+ project. * * PEER DEPENDENCY: React 18+ must be provided by the consuming application. * The agentshield package itself does NOT include React — this component * is for consumers who want to embed the MiniClaw dashboard in their UI. * * Features: * - Dark theme, minimal aesthetic * - Prompt input with submit * - Deterministic fallback response display with room for future streaming upgrades * - Session status indicator * - Security events panel * - Tool whitelist display * * Usage: * import { MiniClawDashboard } from '@agentshield/miniclaw/dashboard'; * */ import React, { useState, useEffect, useCallback, useRef } from "react"; // ─── Types (local to component — no import from types.ts needed) ── type SessionStatus = "idle" | "active" | "error"; interface SessionInfo { readonly sessionId: string; readonly createdAt: string; readonly allowedTools: ReadonlyArray; readonly maxDuration: number; } interface SecurityEventDisplay { readonly type: string; readonly details: string; readonly timestamp: string; } interface PromptResponseDisplay { readonly response: string; readonly toolCalls: ReadonlyArray<{ readonly tool: string; readonly result: string; }>; readonly duration: number; } interface MiniClawDashboardProps { /** Base URL of the MiniClaw server (e.g., "http://localhost:3847") */ readonly endpoint: string; /** Optional custom title for the dashboard */ readonly title?: string; } // ─── Styles ─────────────────────────────────────────────── /** * Inline styles for the dashboard. * * WHY inline styles (not CSS modules/Tailwind): This component must work * when dropped into any React project without requiring a CSS build pipeline. * Inline styles are self-contained and have no external dependencies. */ const styles = { container: { fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', monospace", backgroundColor: "#0d1117", color: "#c9d1d9", borderRadius: "12px", border: "1px solid #30363d", padding: "24px", maxWidth: "800px", margin: "0 auto", } as const, header: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "20px", paddingBottom: "16px", borderBottom: "1px solid #21262d", } as const, title: { fontSize: "18px", fontWeight: 600, color: "#f0f6fc", margin: 0, } as const, statusBadge: { padding: "4px 12px", borderRadius: "12px", fontSize: "12px", fontWeight: 500, } as const, statusIdle: { backgroundColor: "#1f2937", color: "#9ca3af", } as const, statusActive: { backgroundColor: "#064e3b", color: "#6ee7b7", } as const, statusError: { backgroundColor: "#7f1d1d", color: "#fca5a5", } as const, promptArea: { marginBottom: "20px", } as const, promptInput: { width: "100%", padding: "12px 16px", backgroundColor: "#161b22", color: "#c9d1d9", border: "1px solid #30363d", borderRadius: "8px", fontSize: "14px", fontFamily: "inherit", outline: "none", resize: "none" as const, minHeight: "80px", boxSizing: "border-box" as const, } as const, submitButton: { marginTop: "8px", padding: "8px 20px", backgroundColor: "#238636", color: "#ffffff", border: "none", borderRadius: "6px", fontSize: "14px", fontWeight: 500, cursor: "pointer", } as const, submitButtonDisabled: { backgroundColor: "#21262d", color: "#484f58", cursor: "not-allowed", } as const, responseArea: { backgroundColor: "#161b22", border: "1px solid #21262d", borderRadius: "8px", padding: "16px", marginBottom: "20px", minHeight: "100px", maxHeight: "400px", overflowY: "auto" as const, whiteSpace: "pre-wrap" as const, fontSize: "13px", lineHeight: "1.6", } as const, sectionTitle: { fontSize: "14px", fontWeight: 600, color: "#f0f6fc", marginBottom: "8px", marginTop: "16px", } as const, eventList: { listStyle: "none", padding: 0, margin: 0, } as const, eventItem: { padding: "8px 12px", backgroundColor: "#161b22", border: "1px solid #21262d", borderRadius: "6px", marginBottom: "4px", fontSize: "12px", display: "flex", justifyContent: "space-between", alignItems: "center", } as const, eventType: { color: "#f85149", fontWeight: 500, fontSize: "11px", textTransform: "uppercase" as const, } as const, eventTime: { color: "#484f58", fontSize: "11px", } as const, toolList: { display: "flex", flexWrap: "wrap" as const, gap: "6px", marginTop: "8px", } as const, toolBadge: { padding: "2px 8px", borderRadius: "4px", fontSize: "12px", fontWeight: 500, } as const, toolSafe: { backgroundColor: "#064e3b", color: "#6ee7b7", } as const, toolGuarded: { backgroundColor: "#78350f", color: "#fcd34d", } as const, toolRestricted: { backgroundColor: "#7f1d1d", color: "#fca5a5", } as const, emptyState: { color: "#484f58", fontStyle: "italic" as const, textAlign: "center" as const, padding: "24px", } as const, duration: { color: "#484f58", fontSize: "12px", marginTop: "8px", } as const, sessionButton: { padding: "6px 14px", backgroundColor: "#21262d", color: "#c9d1d9", border: "1px solid #30363d", borderRadius: "6px", fontSize: "12px", cursor: "pointer", marginRight: "8px", } as const, footer: { marginTop: "20px", paddingTop: "12px", borderTop: "1px solid #21262d", display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: "11px", color: "#484f58", } as const, } as const; // ─── Subcomponents ──────────────────────────────────────── function StatusBadge({ status }: { readonly status: SessionStatus }): React.ReactElement { const statusStyles = { idle: styles.statusIdle, active: styles.statusActive, error: styles.statusError, }; return ( {status.toUpperCase()} ); } function SecurityEventsPanel({ events, }: { readonly events: ReadonlyArray; }): React.ReactElement { return (

Security Events

{events.length === 0 ? (

No security events recorded

) : (
    {events.map((event, index) => (
  • {event.type} {event.details}
    {new Date(event.timestamp).toLocaleTimeString()}
  • ))}
)}
); } function ToolWhitelistPanel({ tools, }: { readonly tools: ReadonlyArray; }): React.ReactElement { // Categorize tools by known risk levels for display // WHY hardcoded mapping: The dashboard doesn't need to query the server // for risk levels — they are part of the tool definition and don't change. const riskMap: Readonly> = { read: "safe", search: "safe", list: "safe", write: "guarded", edit: "guarded", glob: "guarded", bash: "restricted", network: "restricted", external_api: "restricted", }; const badgeStyle = (tool: string): Record => { const risk = riskMap[tool] ?? "safe"; switch (risk) { case "guarded": return styles.toolGuarded; case "restricted": return styles.toolRestricted; default: return styles.toolSafe; } }; return (

Allowed Tools

{tools.length === 0 ? (

No active session

) : (
{tools.map((tool) => ( {tool} ))}
)}
); } // ─── Main Dashboard Component ───────────────────────────── /** * MiniClaw Dashboard — the primary UI component. * * Manages session lifecycle, prompt submission, and security event display. * All communication with the MiniClaw server goes through the configured endpoint. */ export function MiniClawDashboard({ endpoint, title = "MiniClaw", }: MiniClawDashboardProps): React.ReactElement { const [status, setStatus] = useState("idle"); const [session, setSession] = useState(null); const [prompt, setPrompt] = useState(""); const [response, setResponse] = useState(null); const [events, setEvents] = useState>([]); const [isSubmitting, setIsSubmitting] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const responseRef = useRef(null); // Create a new session const createSession = useCallback(async (): Promise => { try { setStatus("active"); setErrorMessage(null); const res = await fetch(`${endpoint}/api/session`, { method: "POST" }); if (!res.ok) { throw new Error(`Failed to create session: ${res.statusText}`); } const data = (await res.json()) as SessionInfo; setSession(data); } catch (error) { setStatus("error"); const message = error instanceof Error ? error.message : "Failed to create session"; setErrorMessage(message); } }, [endpoint]); // Destroy the current session const destroySession = useCallback(async (): Promise => { if (!session) return; try { await fetch(`${endpoint}/api/session/${session.sessionId}`, { method: "DELETE", }); } catch { // Best effort cleanup } setSession(null); setStatus("idle"); setResponse(null); setEvents([]); setErrorMessage(null); }, [endpoint, session]); // Submit a prompt const submitPrompt = useCallback(async (): Promise => { if (!session || !prompt.trim() || isSubmitting) return; setIsSubmitting(true); setErrorMessage(null); try { const res = await fetch(`${endpoint}/api/prompt`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId: session.sessionId, prompt: prompt.trim(), }), }); if (!res.ok) { throw new Error(`Prompt failed: ${res.statusText}`); } const data = (await res.json()) as PromptResponseDisplay; setResponse(data); setPrompt(""); // Fetch updated security events const eventsRes = await fetch( `${endpoint}/api/events/${session.sessionId}` ); if (eventsRes.ok) { const eventsData = (await eventsRes.json()) as { events: ReadonlyArray; }; setEvents(eventsData.events); } } catch (error) { setStatus("error"); const message = error instanceof Error ? error.message : "Failed to submit prompt"; setErrorMessage(message); } finally { setIsSubmitting(false); } }, [endpoint, session, prompt, isSubmitting]); // Handle keyboard shortcut (Cmd/Ctrl + Enter to submit) const handleKeyDown = useCallback( (e: React.KeyboardEvent): void => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); submitPrompt(); } }, [submitPrompt] ); // Auto-scroll response area when new content arrives useEffect(() => { if (responseRef.current) { responseRef.current.scrollTop = responseRef.current.scrollHeight; } }, [response]); return (
{/* Header with status */}

{title}

{!session ? ( ) : ( )}
{/* Error display */} {errorMessage && (
{errorMessage}
)} {/* Prompt input */}