diff --git a/src/components/AgentNode.tsx b/src/components/AgentNode.tsx index 2b9a252..e5f9a12 100644 --- a/src/components/AgentNode.tsx +++ b/src/components/AgentNode.tsx @@ -82,13 +82,10 @@ export function AgentNode({ agent, isHighlighted, onHover, onClick }: AgentNodeP }} whileHover={{ scale: 1.05 }} > - {/* Glow ring for active agents */} + {/* Glow ring for active agents - CSS animation instead of repeat: Infinity */} {isActive && ( - )} diff --git a/src/components/ConnectionsLayer.tsx b/src/components/ConnectionsLayer.tsx index 13a8308..70f55a6 100644 --- a/src/components/ConnectionsLayer.tsx +++ b/src/components/ConnectionsLayer.tsx @@ -1,6 +1,5 @@ -import { motion } from 'framer-motion' +import { useRef, useEffect, useState } from 'react' import type { Connection, Agent } from '../types/agent' -import { cn } from '../lib/utils' interface ConnectionsLayerProps { connections: Connection[] @@ -16,12 +15,30 @@ const connectionTypeStyles = { } export function ConnectionsLayer({ connections, agents, highlightedAgentId }: ConnectionsLayerProps) { + const containerRef = useRef(null) + const [dimensions, setDimensions] = useState({ width: 800, height: 600 }) + + useEffect(() => { + const container = containerRef.current + if (!container) return + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { width, height } = entry.contentRect + setDimensions({ width, height }) + } + }) + + resizeObserver.observe(container) + return () => resizeObserver.disconnect() + }, []) + const getAgentPosition = (agentId: string) => { const agent = agents.find(a => a.id === agentId) if (!agent) return { x: 0, y: 0 } return { - x: (agent.position.x / 100) * 800, // Canvas width - y: (agent.position.y / 100) * 600 // Canvas height + x: (agent.position.x / 100) * dimensions.width, + y: (agent.position.y / 100) * dimensions.height } } @@ -31,86 +48,74 @@ export function ConnectionsLayer({ connections, agents, highlightedAgentId }: Co } return ( - - - {/* Glow filter */} - - - - - - - - - {/* Arrow marker */} - - - - +
+ + + + + + + + + + + + + + - {connections.map((connection) => { - const from = getAgentPosition(connection.from) - const to = getAgentPosition(connection.to) - const styles = connectionTypeStyles[connection.type] - const isHighlighted = isConnectionHighlighted(connection) - const isActive = connection.active + {connections.map((connection) => { + const from = getAgentPosition(connection.from) + const to = getAgentPosition(connection.to) + const styles = connectionTypeStyles[connection.type] + const isHighlighted = isConnectionHighlighted(connection) + const isActive = connection.active - // Calculate control point for curved line - const midX = (from.x + to.x) / 2 - const midY = (from.y + to.y) / 2 - const dx = to.x - from.x - const dy = to.y - from.y - const ctrlX = midX - dy * 0.2 - const ctrlY = midY + dx * 0.2 + const midX = (from.x + to.x) / 2 + const midY = (from.y + to.y) / 2 + const dx = to.x - from.x + const dy = to.y - from.y + const ctrlX = midX - dy * 0.2 + const ctrlY = midY + dx * 0.2 - return ( - - {/* Shadow/glow line */} - {isHighlighted && ( - + {isHighlighted && ( + + )} + + - )} - - {/* Main line */} - - - ) - })} - + + ) + })} + +
) } diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index 6173fa8..99da237 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -1,10 +1,10 @@ +import { useState, useEffect, useMemo } from 'react' import { Activity, AlertTriangle, Clock, CheckCircle2, Zap, RefreshCw, Wifi, WifiOff } from 'lucide-react' import type { Agent } from '../types/agent' -import type { GatewayHealth } from '../lib/openclaw-api' interface StatusBarProps { agents: Agent[] - health?: GatewayHealth + health?: { status: string; uptime: number; sessions: number } lastUpdated?: Date | null loading?: boolean error?: string | null @@ -13,18 +13,30 @@ interface StatusBarProps { } export function StatusBar({ agents, health, lastUpdated, loading = false, error, connected = false, onRefresh }: StatusBarProps) { - const stats = { + const [currentTime, setCurrentTime] = useState(new Date()) + + useEffect(() => { + const interval = setInterval(() => { + setCurrentTime(new Date()) + }, 1000) + return () => clearInterval(interval) + }, []) + + const stats = useMemo(() => ({ active: agents.filter(a => a.status === 'active').length, thinking: agents.filter(a => a.status === 'thinking').length, idle: agents.filter(a => a.status === 'idle').length, blocked: agents.filter(a => a.status === 'blocked').length, completed: agents.filter(a => a.status === 'completed').length, - } + }), [agents]) - const totalTasks = agents.reduce((acc, a) => acc + a.stats.tasksCompleted + a.stats.tasksInProgress, 0) - const completedTasks = agents.reduce((acc, a) => acc + a.stats.tasksCompleted, 0) + const taskStats = useMemo(() => { + const totalTasks = agents.reduce((acc, a) => acc + (a.stats?.tasksCompleted || 0) + (a.stats?.tasksInProgress || 0), 0) + const completedTasks = agents.reduce((acc, a) => acc + (a.stats?.tasksCompleted || 0), 0) + return { totalTasks, completedTasks } + }, [agents]) - const progress = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0 + const progress = taskStats.totalTasks > 0 ? (taskStats.completedTasks / taskStats.totalTasks) * 100 : 0 const formatUptime = (ms: number) => { if (!ms) return 'β€”' @@ -36,9 +48,10 @@ export function StatusBar({ agents, health, lastUpdated, loading = false, error, return `${s}s` } + const locale = typeof navigator !== 'undefined' ? navigator.language : 'es-CL' + return (
- {/* Logo / Title */}
@@ -49,9 +62,7 @@ export function StatusBar({ agents, health, lastUpdated, loading = false, error,
- {/* Stats */}
- {/* Gateway Status */}
{connected ? ( @@ -68,7 +79,6 @@ export function StatusBar({ agents, health, lastUpdated, loading = false, error,
- {/* Sessions */} {health && health.sessions > 0 && (
@@ -77,14 +87,13 @@ export function StatusBar({ agents, health, lastUpdated, loading = false, error,
)} - {/* Task Progress */}

Tareas

- {completedTasks} + {taskStats.completedTasks} / - {totalTasks} + {taskStats.totalTasks}

@@ -95,7 +104,6 @@ export function StatusBar({ agents, health, lastUpdated, loading = false, error,
- {/* Agent Status Pills */}
@@ -124,7 +132,6 @@ export function StatusBar({ agents, health, lastUpdated, loading = false, error,
- {/* Time / Refresh */}
diff --git a/src/hooks/useOpenClaw.ts b/src/hooks/useOpenClaw.ts index dbc77ef..f6025e7 100644 --- a/src/hooks/useOpenClaw.ts +++ b/src/hooks/useOpenClaw.ts @@ -1,117 +1,80 @@ -import { useState, useEffect, useCallback, useRef } from 'react' -import { getOpenClawClient, resetOpenClawClient, type OpenClawState } from '../lib/openclaw-api' -import type { Agent } from '../types/agent' -import { agents as staticAgents } from '../data/agents' +import { useState, useEffect, useCallback, useRef } from 'react'; +import { openclawClient } from '../lib/openclaw-api'; +import type { Agent, TimelineEvent } from '../types/agent'; -// Layout positions para cada agente (fijas en el canvas) -const AGENT_POSITIONS: Record = { - main: { x: 50, y: 50 }, - architect: { x: 25, y: 30 }, - coder: { x: 15, y: 55 }, - designer: { x: 35, y: 70 }, - assistant: { x: 75, y: 60 }, - reviewer: { x: 65, y: 35 }, +interface UseOpenClawReturn { + agents: Agent[]; + events: TimelineEvent[]; + health: { status: string; uptime: number; sessions: number } | null; + isLoading: boolean; + error: string | null; + lastUpdate: Date | null; + refresh: () => void; } -// Avatar y role por defecto desde los datos estΓ‘ticos -const AGENT_META: Record = {} -for (const a of staticAgents) { - AGENT_META[a.id] = { avatar: a.avatar ?? 'πŸ€–', role: a.role } -} +const POLLING_INTERVAL = 30000; -export interface UseOpenClawOptions { - pollInterval?: number - gatewayUrl?: string - token?: string -} +export function useOpenClaw(): UseOpenClawReturn { + const [agents, setAgents] = useState([]); + const [events, setEvents] = useState([]); + const [health, setHealth] = useState<{ status: string; uptime: number; sessions: number } | null>(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [lastUpdate, setLastUpdate] = useState(null); + + const intervalRef = useRef(null); + const isMountedRef = useRef(true); -export interface UseOpenClawResult { - agents: Agent[] - connections: import('../types/agent').Connection[] - events: import('../types/agent').TimelineEvent[] - health: import('../lib/openclaw-api').GatewayHealth - lastUpdated: Date | null - loading: boolean - error: string | null - connected: boolean - refetch: () => void -} + const fetchData = useCallback(async () => { + try { + const [agentsData, eventsData, healthData] = await Promise.all([ + openclawClient.fetchAgents(), + openclawClient.fetchEvents(), + openclawClient.fetchHealth(), + ]); -function openClawAgentToAgent(ocAgent: import('../lib/openclaw-api').OpenClawAgent): Agent { - const pos = AGENT_POSITIONS[ocAgent.id] ?? { x: 50, y: 50 } - const meta = AGENT_META[ocAgent.id] ?? { avatar: 'πŸ€–', role: 'assistant' } - const incomingConnections = ['main', 'architect', 'coder', 'designer', 'assistant', 'reviewer'].filter( - id => id !== ocAgent.id - ) + if (isMountedRef.current) { + setAgents(agentsData); + setEvents(eventsData); + setHealth(healthData); + setError(null); + setLastUpdate(new Date()); + setIsLoading(false); + } + } catch (err) { + if (isMountedRef.current) { + setError(err instanceof Error ? err.message : 'Failed to connect to gateway'); + setIsLoading(false); + } + } + }, []); - return { - id: ocAgent.id, - name: ocAgent.name, - role: meta.role, - status: ocAgent.status, - position: pos, - avatar: meta.avatar, - currentTask: ocAgent.currentTask, - connections: incomingConnections, - stats: ocAgent.stats, - } -} - -export function useOpenClaw(opts: UseOpenClawOptions = {}): UseOpenClawResult { - const { pollInterval = 30_000, gatewayUrl, token } = opts - - const [state, setState] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [connected, setConnected] = useState(false) - const pollTimerRef = useRef | null>(null) - - const startPolling = useCallback(() => { - if (pollTimerRef.current) clearInterval(pollTimerRef.current) - pollTimerRef.current = setInterval(() => { - getOpenClawClient(gatewayUrl, token).poll() - }, pollInterval) - }, [pollInterval, gatewayUrl, token]) + const refresh = useCallback(() => { + fetchData(); + }, [fetchData]); useEffect(() => { - const client = getOpenClawClient(gatewayUrl, token) - client.setPollInterval(pollInterval) + isMountedRef.current = true; + fetchData(); - client.onUpdate((newState) => { - setState(newState) - setLoading(false) - setError(null) - setConnected(client.isHealthy()) - }) - - // Initial poll - client.poll().catch((err: Error) => { - setError(err.message ?? 'Error conectando al gateway') - setLoading(false) - }) - - startPolling() + intervalRef.current = window.setInterval(fetchData, POLLING_INTERVAL); return () => { - if (pollTimerRef.current) clearInterval(pollTimerRef.current) - resetOpenClawClient() - } - }, [pollInterval, gatewayUrl, token, startPolling]) - - // Convertir estado de OpenClaw β†’ agentes del dashboard - const agents: Agent[] = state - ? state.agents.map(openClawAgentToAgent) - : staticAgents.map(a => ({ ...a, status: 'idle' as const })) + isMountedRef.current = false; + if (intervalRef.current !== null) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + }, [fetchData]); return { agents, - connections: state?.connections ?? [], - events: state?.events ?? [], - health: state?.health ?? { healthy: false, uptime: 0, agents: 0, sessions: 0 }, - lastUpdated: state?.lastUpdated ?? null, - loading, + events, + health, + isLoading, error, - connected, - refetch: () => getOpenClawClient(gatewayUrl, token).poll(), - } + lastUpdate, + refresh, + }; } diff --git a/src/index.css b/src/index.css index 88d3d0e..6d1eb97 100644 --- a/src/index.css +++ b/src/index.css @@ -112,3 +112,23 @@ body { outline: 2px solid rgba(59, 130, 246, 0.5); outline-offset: 2px; } + +/* Node pulse animation - replaces repeat: Infinity */ +@keyframes node-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.node-pulse { + animation: node-pulse 2s ease-in-out infinite; +} + +/* Glow effect for active nodes */ +@keyframes glow-pulse { + 0%, 100% { transform: scale(1); opacity: 0.3; filter: blur(20px); } + 50% { transform: scale(1.3); opacity: 0.5; filter: blur(25px); } +} + +.node-glow { + animation: glow-pulse 2s ease-in-out infinite; +} diff --git a/src/lib/openclaw-api.ts b/src/lib/openclaw-api.ts index 5b353fe..e259575 100644 --- a/src/lib/openclaw-api.ts +++ b/src/lib/openclaw-api.ts @@ -1,313 +1,119 @@ -/** - * OpenClaw Gateway API Client - * - * Provee mΓ©todos para obtener estado de agentes, sesiones y eventos - * desde el gateway de OpenClaw via HTTP polling. - * - * Para uso en browser, el gateway debe estar expuesto via reverse proxy. - * Configurar VITE_OPENCLAW_GATEWAY_URL y VITE_OPENCLAW_GATEWAY_TOKEN en .env - */ +import type { Agent, Connection, TimelineEvent } from '../types/agent'; -import type { Agent, Connection, TimelineEvent } from '../types/agent' +const GATEWAY_URL = import.meta.env.VITE_OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789'; +const GATEWAY_TOKEN = import.meta.env.VITE_OPENCLAW_GATEWAY_TOKEN || ''; -export interface GatewayHealth { - healthy: boolean - uptime: number - agents: number - sessions: number +interface GatewayHealth { + status: string; + uptime: number; + sessions: number; + agents: number; } -export interface OpenClawAgent { - id: string - name: string - role: string - status: Agent['status'] - workspace: string - model: string - sessionCount: number - lastActive: number - currentTask?: string - stats: { - tasksCompleted: number - tasksInProgress: number - tasksBlocked: number - } +interface GatewayAgentsResponse { + agents: Array<{ + id: string; + name: string; + status: string; + model: string; + }>; } -export interface OpenClawState { - agents: OpenClawAgent[] - connections: Connection[] - events: TimelineEvent[] - health: GatewayHealth - lastUpdated: Date -} - -// Mapeo de IDs de agente OpenClaw β†’ datos del dashboard -const AGENT_META: Record = { - main: { name: 'Erwin', avatar: '🎯', role: 'orchestrator' }, - architect: { name: 'Bulma', avatar: 'πŸ—οΈ', role: 'architect' }, - coder: { name: 'Rocket', avatar: 'πŸš€', role: 'developer' }, - designer: { name: 'Hiro', avatar: '🎨', role: 'designer' }, - assistant: { name: 'Claudia', avatar: 'πŸ“', role: 'assistant' }, - reviewer: { name: 'Sherlock', avatar: 'πŸ”', role: 'reviewer' }, -} - -// Relaciones estΓ‘ticas entre agentes (reporting lines) -const AGENT_CONNECTIONS: Connection[] = [ - { id: 'c1', from: 'main', to: 'architect', type: 'delegates', active: true }, - { id: 'c2', from: 'main', to: 'coder', type: 'delegates', active: true }, - { id: 'c3', from: 'main', to: 'designer', type: 'delegates', active: false }, - { id: 'c4', from: 'main', to: 'reviewer', type: 'requests', active: false }, - { id: 'c5', from: 'main', to: 'assistant', type: 'requests', active: false }, - { id: 'c6', from: 'architect', to: 'main', type: 'reports', active: true }, - { id: 'c7', from: 'architect', to: 'coder', type: 'transfers', active: true }, - { id: 'c8', from: 'architect', to: 'designer', type: 'transfers', active: false }, - { id: 'c9', from: 'coder', to: 'main', type: 'reports', active: true }, - { id: 'c10', from: 'coder', to: 'reviewer', type: 'requests', active: true }, - { id: 'c11', from: 'designer', to: 'main', type: 'reports', active: false }, - { id: 'c12', from: 'reviewer', to: 'main', type: 'reports', active: true }, - { id: 'c13', from: 'reviewer', to: 'assistant', type: 'requests', active: false }, - { id: 'c14', from: 'assistant', to: 'main', type: 'reports', active: true }, -] - -function nextEventId(): string { - return `evt-${Date.now()}-${Math.random().toString(36).slice(2, 7)}` -} - -function guessStatus(sessionCount: number, lastActive: number): Agent['status'] { - if (sessionCount === 0) return 'idle' - const since = Date.now() - lastActive - if (since > 5 * 60 * 1000) return 'idle' - if (sessionCount > 0) return 'active' - return 'thinking' -} - -function buildOpenClawAgent( - id: string, - sessionCount: number, - lastActive: number, - currentTask?: string -): OpenClawAgent { - const meta = AGENT_META[id] ?? { name: id, avatar: 'πŸ€–', role: 'assistant' as const } - const status = guessStatus(sessionCount, lastActive) - return { - id, - name: meta.name, - role: meta.role, - status, - workspace: '', - model: '', - sessionCount, - lastActive, - currentTask, - stats: { - tasksCompleted: Math.floor(Math.random() * 20) + (status === 'completed' ? 5 : 0), - tasksInProgress: status === 'active' || status === 'thinking' ? Math.floor(Math.random() * 5) : 0, - tasksBlocked: status === 'blocked' ? Math.floor(Math.random() * 3) : 0, - }, - } -} - -function buildConnections(agentIds: string[]): Connection[] { - const validIds = new Set(agentIds) - return AGENT_CONNECTIONS.filter(c => validIds.has(c.from) && validIds.has(c.to)) -} - -// ─── HTTP API Client ───────────────────────────────────────────────────────── - export class OpenClawGatewayClient { - private baseUrl: string - private token: string - private pollInterval = 30_000 - private pollTimer: ReturnType | null = null - private onStateChange: ((state: OpenClawState) => void) | null = null - private lastHealth: GatewayHealth = { healthy: false, uptime: 0, agents: 6, sessions: 0 } - private knownAgentIds = ['main', 'architect', 'coder', 'designer', 'assistant', 'reviewer'] + private baseUrl: string; + private token: string; - constructor(baseUrl: string, token: string) { - // Normalize: ensure http/https prefix - this.baseUrl = baseUrl.replace(/^ws(s)?:\/\//, 'http$1://') - this.token = token + constructor(baseUrl: string = GATEWAY_URL, token: string = GATEWAY_TOKEN) { + this.baseUrl = baseUrl; + this.token = token; } - private async fetch(path: string, options?: RequestInit): Promise { - const url = `${this.baseUrl}${path}` - const resp = await fetch(url, { - ...options, - headers: { - 'Content-Type': 'application/json', - ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}), - ...options?.headers, - }, - }) - if (!resp.ok) { - throw new Error(`Gateway ${resp.status}: ${resp.statusText}`) + private async fetch(path: string): Promise { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (this.token) { + headers['Authorization'] = `Bearer ${this.token}`; } - return resp.json() as Promise + + const response = await fetch(`${this.baseUrl}${path}`, { headers }); + + if (!response.ok) { + throw new Error(`Gateway error: ${response.status} ${response.statusText}`); + } + + return response.json(); } - /** - * Obtiene estado de salud del gateway y sesiones activas. - * Endpoint: GET /healthz (o similar endpoint interno) - */ async fetchHealth(): Promise { + return this.fetch('/api/health'); + } + + async fetchAgents(): Promise { try { - // Tratamos de obtener /v1/models como health check - const data = await this.fetch<{ data?: unknown[] }>('/v1/models') - void data // just to verify gateway is reachable - return { - healthy: true, - uptime: this.lastHealth.uptime, - agents: this.knownAgentIds.length, - sessions: this.lastHealth.sessions, - } + const data = await this.fetch('/api/agents'); + return data.agents.map((agent, index) => ({ + id: agent.id, + name: agent.name, + role: this.getRoleFromAgentId(agent.id), + status: agent.status === 'active' ? 'active' : agent.status === 'busy' ? 'busy' : 'idle', + queue: 0, + icon: this.getIconFromIndex(index), + x: this.getAgentX(index), + y: this.getAgentY(index), + })); } catch { - // Gateway no reachable desde el browser (CORS o no expuesto) - // Devolvemos ΓΊltimo estado conocido con healthy=false - return { ...this.lastHealth, healthy: false } + return this.getDefaultAgents(); } } - /** - * Obtiene lista de sesiones activas del gateway. - */ - async fetchSessions(): Promise<{ sessions: number; agents: Record }> { + async fetchEvents(): Promise { try { - // GET /sessions o similar - si no existe, usamos estimate - // Como no hay endpoint REST pΓΊblico, usamos el ΓΊltimo valor conocido - return { - sessions: this.lastHealth.sessions || 11, - agents: Object.fromEntries(this.knownAgentIds.map(id => [id, 1])), - } + const data = await this.fetch<{ events: TimelineEvent[] }>('/api/events'); + return data.events || []; } catch { - return { - sessions: this.lastHealth.sessions || 11, - agents: Object.fromEntries(this.knownAgentIds.map(id => [id, 1])), - } + return []; } } - /** - * Obtiene agentes con estado estimado basado en sesiones. - */ - async fetchAgents(): Promise { - const sessions = await this.fetchSessions() - const perAgent = Math.max(1, Math.floor(sessions.sessions / this.knownAgentIds.length)) - - return this.knownAgentIds.map((id) => - buildOpenClawAgent(id, perAgent, Date.now() - Math.random() * 600_000) - ) + private getRoleFromAgentId(id: string): string { + const roles: Record = { + orchestrator: 'Coordina flujo y delegaciΓ³n', + architect: 'Define soluciΓ³n y tareas', + coder: 'Implementa cambios', + reviewer: 'Revisa calidad y riesgos', + docs: 'Documenta decisiones y cambios', + assistant: 'Asistente general', + }; + return roles[id] || 'Agente'; } - /** - * Obtiene eventos recientes del timeline. - */ - async fetchEvents(agents: OpenClawAgent[]): Promise { - const events: TimelineEvent[] = [] - for (const agent of agents) { - if (agent.status === 'active' || agent.status === 'thinking') { - events.push({ - id: nextEventId(), - agentId: agent.id, - timestamp: new Date(Date.now() - Math.random() * 300_000), - type: 'task_start', - description: `${agent.name} estΓ‘ ${agent.status === 'thinking' ? 'analizando' : 'ejecutando tarea'}`, - }) - } - if (agent.stats.tasksCompleted > 0) { - events.push({ - id: nextEventId(), - agentId: agent.id, - timestamp: new Date(Date.now() - Math.random() * 600_000), - type: 'task_complete', - description: `${agent.name} completΓ³ ${agent.stats.tasksCompleted} tareas`, - }) - } - } - return events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()).slice(0, 20) + private getIconFromIndex(index: number) { + const icons = ['Workflow', 'Layers3', 'Bot', 'ShieldCheck', 'FileText', 'Activity']; + return icons[index % icons.length]; } - /** - * Polling principal: obtiene todo el estado del gateway. - */ - async poll(): Promise { - try { - const [agents, health] = await Promise.allSettled([ - this.fetchAgents(), - this.fetchHealth(), - ]) - - const openClawAgents = agents.status === 'fulfilled' ? agents.value : [] - const gwHealth = health.status === 'fulfilled' ? health.value : this.lastHealth - - // Actualizar health con sessions count si estΓ‘ disponible - if (gwHealth.sessions > 0) { - this.lastHealth = gwHealth - } else { - // Estimar sesiones desde el health - this.lastHealth = { ...this.lastHealth, healthy: gwHealth.healthy } - } - - const events = await this.fetchEvents(openClawAgents) - - const state: OpenClawState = { - agents: openClawAgents, - connections: buildConnections(openClawAgents.map(a => a.id)), - events, - health: this.lastHealth, - lastUpdated: new Date(), - } - - this.onStateChange?.(state) - } catch (err) { - console.error('[OpenClawGateway] poll error:', err) - } + private getAgentX(index: number): number { + const positions = [10, 36, 36, 66, 66, 66]; + return positions[index] || 50; } - setPollInterval(ms: number): void { - this.pollInterval = ms + private getAgentY(index: number): number { + const positions = [38, 12, 64, 24, 64, 50]; + return positions[index] || 50; } - onUpdate(cb: (state: OpenClawState) => void): void { - this.onStateChange = cb - } - - startPolling(): void { - this.poll() - this.pollTimer = setInterval(() => this.poll(), this.pollInterval) - } - - stopPolling(): void { - if (this.pollTimer) { - clearInterval(this.pollTimer) - this.pollTimer = null - } - } - - isHealthy(): boolean { - return this.lastHealth.healthy - } - - getLastHealth(): GatewayHealth { - return this.lastHealth + private getDefaultAgents(): Agent[] { + return [ + { id: 'erwin', name: 'Erwin', role: 'Orquestador', status: 'active', queue: 0, icon: 'Workflow', x: 10, y: 38 }, + { id: 'bulma', name: 'Bulma', role: 'Arquitecto', status: 'active', queue: 0, icon: 'Layers3', x: 36, y: 12 }, + { id: 'rocket', name: 'Rocket', role: 'Desarrollador', status: 'busy', queue: 0, icon: 'Bot', x: 36, y: 64 }, + { id: 'sherlock', name: 'Sherlock', role: 'Revisor', status: 'idle', queue: 0, icon: 'ShieldCheck', x: 66, y: 24 }, + { id: 'hiro', name: 'Hiro', role: 'DiseΓ±ador', status: 'idle', queue: 0, icon: 'FileText', x: 66, y: 64 }, + { id: 'claudia', name: 'Claudia', role: 'Asistente', status: 'idle', queue: 0, icon: 'Activity', x: 66, y: 50 }, + ]; } } -// ─── Singleton instance ─────────────────────────────────────────────────────── - -let _client: OpenClawGatewayClient | null = null - -export function getOpenClawClient(gatewayUrl?: string, token?: string): OpenClawGatewayClient { - if (!_client) { - const url = gatewayUrl - ?? (typeof window !== 'undefined' ? window.location.origin : 'http://127.0.0.1:18789') - const tk = token ?? '' - _client = new OpenClawGatewayClient(url, tk) - } - return _client -} - -export function resetOpenClawClient(): void { - _client?.stopPolling() - _client = null -} +export const openclawClient = new OpenClawGatewayClient();