zeroclaw/web/src/hooks/useSSE.ts
Zeki Kocabıyık 79337c76e8 feat(gateway): add embedded web dashboard with React frontend
Add a complete web management panel for ZeroClaw, served directly from
the binary via rust-embed. The dashboard provides real-time monitoring,
agent chat, configuration editing, and system diagnostics — all
accessible at http://localhost:5555/ after pairing.

Backend (Rust):
- Add 15+ REST API endpoints under /api/* with bearer token auth
- Add WebSocket agent chat at /ws/chat with query param auth
- Add SSE event stream at /api/events via BroadcastObserver
- Add rust-embed static file serving at /_app/* with SPA fallback
- Extend AppState with tools_registry, cost_tracker, event_tx
- Extract doctor::diagnose() for structured diagnostic results
- Add Serialize derives to IntegrationStatus, CliCategory, DiscoveredCli

Frontend (React + Vite + Tailwind CSS):
- 10 dashboard pages: Dashboard, AgentChat, Tools, Cron, Integrations,
  Memory, Config, Cost, Logs, Doctor
- WebSocket client with auto-reconnect for agent chat
- SSE client (fetch-based, supports auth headers) for live events
- Full EN/TR internationalization (~190 translation keys)
- Dark theme with responsive layouts
- Auth flow via 6-digit pairing code, token stored in localStorage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 16:14:01 +08:00

125 lines
3.3 KiB
TypeScript

import { useState, useEffect, useRef, useCallback } from 'react';
import { SSEClient, type SSEClientOptions } from '../lib/sse';
import type { SSEEvent } from '../types/api';
export type SSEConnectionStatus = 'disconnected' | 'connecting' | 'connected';
export interface UseSSEResult {
/** Array of all events received during this session. */
events: SSEEvent[];
/** Current connection status. */
status: SSEConnectionStatus;
/** Manually connect (called automatically on mount). */
connect: () => void;
/** Manually disconnect. */
disconnect: () => void;
/** Clear the event history. */
clearEvents: () => void;
}
export interface UseSSEOptions extends SSEClientOptions {
/** If false, do not connect automatically on mount. Default true. */
autoConnect?: boolean;
/** Maximum number of events to keep in the buffer. Default 500. */
maxEvents?: number;
/** Optional filter: only keep events whose type matches. */
filterTypes?: string[];
}
/**
* React hook that wraps the SSEClient for live event streaming.
*
* Connects on mount (unless `autoConnect` is false), accumulates incoming
* events, and cleans up on unmount.
*/
export function useSSE(options: UseSSEOptions = {}): UseSSEResult {
const {
autoConnect = true,
maxEvents = 500,
filterTypes,
...sseOptions
} = options;
const clientRef = useRef<SSEClient | null>(null);
const [status, setStatus] = useState<SSEConnectionStatus>('disconnected');
const [events, setEvents] = useState<SSEEvent[]>([]);
// Keep filter in a ref so the callback doesn't need to be recreated
const filterRef = useRef(filterTypes);
filterRef.current = filterTypes;
const maxRef = useRef(maxEvents);
maxRef.current = maxEvents;
// Stable reference to the client across renders
const getClient = useCallback((): SSEClient => {
if (!clientRef.current) {
clientRef.current = new SSEClient(sseOptions);
}
return clientRef.current;
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Setup handlers and optionally connect on mount
useEffect(() => {
const client = getClient();
client.onConnect = () => {
setStatus('connected');
};
client.onEvent = (event: SSEEvent) => {
// Apply type filter if configured
if (filterRef.current && filterRef.current.length > 0) {
if (!filterRef.current.includes(event.type)) return;
}
setEvents((prev) => {
const next = [...prev, event];
// Trim to max buffer size
if (next.length > maxRef.current) {
return next.slice(next.length - maxRef.current);
}
return next;
});
};
client.onError = () => {
setStatus('disconnected');
};
if (autoConnect) {
setStatus('connecting');
client.connect();
}
return () => {
client.disconnect();
clientRef.current = null;
};
}, [getClient, autoConnect]);
const connect = useCallback(() => {
const client = getClient();
setStatus('connecting');
client.connect();
}, [getClient]);
const disconnect = useCallback(() => {
const client = getClient();
client.disconnect();
setStatus('disconnected');
}, [getClient]);
const clearEvents = useCallback(() => {
setEvents([]);
}, []);
return {
events,
status,
connect,
disconnect,
clearEvents,
};
}