feat(web): add gateway, hub, active-agent hooks and real message state
- use-gateway: GatewayClient WebSocket lifecycle with auto-connect/disconnect - use-hub: REST polling for Hub status and agent CRUD operations - use-active-agent: Zustand store for cross-component selected agent state - use-messages: replace mock data with real addUser/addAssistant/clear API Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3ef7d5351c
commit
c5bf56282f
4 changed files with 140 additions and 167 deletions
11
apps/web/app/hooks/use-active-agent.ts
Normal file
11
apps/web/app/hooks/use-active-agent.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { create } from "zustand"
|
||||
|
||||
interface ActiveAgentState {
|
||||
activeAgentId: string | null
|
||||
setActiveAgentId: (id: string | null) => void
|
||||
}
|
||||
|
||||
export const useActiveAgent = create<ActiveAgentState>()((set) => ({
|
||||
activeAgentId: null,
|
||||
setActiveAgentId: (id: string | null) => set({ activeAgentId: id }),
|
||||
}))
|
||||
41
apps/web/app/hooks/use-gateway.ts
Normal file
41
apps/web/app/hooks/use-gateway.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { useEffect, useRef, useState, useCallback } from "react"
|
||||
import { GatewayClient, type ConnectionState, type RoutedMessage } from "@multica/sdk"
|
||||
import { useDeviceId } from "./use-device-id"
|
||||
import { GATEWAY_URL } from "../lib/config"
|
||||
|
||||
interface UseGatewayOptions {
|
||||
onMessage?: (msg: RoutedMessage) => void
|
||||
}
|
||||
|
||||
export function useGateway(options?: UseGatewayOptions) {
|
||||
const deviceId = useDeviceId()
|
||||
const [state, setState] = useState<ConnectionState>("disconnected")
|
||||
const clientRef = useRef<GatewayClient | null>(null)
|
||||
const onMessageRef = useRef(options?.onMessage)
|
||||
onMessageRef.current = options?.onMessage
|
||||
|
||||
useEffect(() => {
|
||||
if (!deviceId) return
|
||||
|
||||
const client = new GatewayClient({
|
||||
url: GATEWAY_URL,
|
||||
deviceId,
|
||||
deviceType: "client",
|
||||
})
|
||||
.onStateChange(setState)
|
||||
.onMessage(msg => onMessageRef.current?.(msg))
|
||||
|
||||
clientRef.current = client
|
||||
client.connect()
|
||||
return () => { client.disconnect() }
|
||||
}, [deviceId])
|
||||
|
||||
const send = useCallback(
|
||||
(to: string, action: string, payload: unknown) => {
|
||||
clientRef.current?.send(to, action, payload)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return { state, send }
|
||||
}
|
||||
65
apps/web/app/hooks/use-hub.ts
Normal file
65
apps/web/app/hooks/use-hub.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { useState, useCallback, useEffect } from "react"
|
||||
import { CONSOLE_URL } from "../lib/config"
|
||||
|
||||
interface HubInfo {
|
||||
hubId: string
|
||||
url: string
|
||||
connectionState: string
|
||||
agentCount: number
|
||||
}
|
||||
|
||||
interface Agent {
|
||||
id: string
|
||||
closed: boolean
|
||||
}
|
||||
|
||||
type HubStatus = "idle" | "loading" | "connected" | "error"
|
||||
|
||||
export function useHub() {
|
||||
const [status, setStatus] = useState<HubStatus>("idle")
|
||||
const [hub, setHub] = useState<HubInfo | null>(null)
|
||||
const [agents, setAgents] = useState<Agent[]>([])
|
||||
|
||||
const fetchHub = useCallback(async () => {
|
||||
setStatus("loading")
|
||||
try {
|
||||
const res = await fetch(`${CONSOLE_URL}/api/hub`)
|
||||
if (!res.ok) throw new Error(res.statusText)
|
||||
const data: HubInfo = await res.json()
|
||||
setHub(data)
|
||||
setStatus(data.connectionState === "registered" ? "connected" : "error")
|
||||
} catch {
|
||||
setStatus("error")
|
||||
setHub(null)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fetchAgents = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`${CONSOLE_URL}/api/agents`)
|
||||
if (res.ok) setAgents(await res.json())
|
||||
} catch { /* silent */ }
|
||||
}, [])
|
||||
|
||||
const createAgent = useCallback(async () => {
|
||||
await fetch(`${CONSOLE_URL}/api/agents`, { method: "POST" })
|
||||
await fetchAgents()
|
||||
}, [fetchAgents])
|
||||
|
||||
const deleteAgent = useCallback(async (id: string) => {
|
||||
await fetch(`${CONSOLE_URL}/api/agents/${id}`, { method: "DELETE" })
|
||||
await fetchAgents()
|
||||
}, [fetchAgents])
|
||||
|
||||
// Auto-fetch hub on mount, agents when connected, poll every 30s
|
||||
useEffect(() => { fetchHub() }, [fetchHub])
|
||||
useEffect(() => {
|
||||
if (status === "connected") fetchAgents()
|
||||
}, [status, fetchAgents])
|
||||
useEffect(() => {
|
||||
const id = setInterval(fetchHub, 30_000)
|
||||
return () => clearInterval(id)
|
||||
}, [fetchHub])
|
||||
|
||||
return { status, hub, agents, fetchHub, createAgent, deleteAgent }
|
||||
}
|
||||
|
|
@ -1,169 +1,25 @@
|
|||
export type Message = {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
};
|
||||
import { useState, useCallback } from "react"
|
||||
import { v7 as uuidv7 } from "uuid"
|
||||
|
||||
const MOCK_MESSAGES: Message[] = [
|
||||
{
|
||||
id: "1",
|
||||
role: "user",
|
||||
content: "Hey! Can you explain how WebSocket connections work?",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
role: "assistant",
|
||||
content: `WebSocket provides **full-duplex** communication over a single TCP connection. Here's how it works:
|
||||
|
||||
1. **Handshake** — The client sends an HTTP upgrade request
|
||||
2. **Connection** — The server agrees and upgrades the protocol
|
||||
3. **Messaging** — Both sides can send data at any time
|
||||
4. **Close** — Either side can close the connection
|
||||
|
||||
A basic example in JavaScript:
|
||||
|
||||
\`\`\`ts
|
||||
const ws = new WebSocket("wss://example.com/ws");
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("Connected");
|
||||
ws.send(JSON.stringify({ type: "hello" }));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("Received:", data);
|
||||
};
|
||||
\`\`\`
|
||||
|
||||
> Unlike HTTP, WebSocket keeps the connection alive so you don't need to poll.`,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
role: "user",
|
||||
content: "What's the difference between Socket.IO and raw WebSocket?",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
role: "assistant",
|
||||
content: `Great question! Here's a comparison:
|
||||
|
||||
| Feature | WebSocket | Socket.IO |
|
||||
|---------|-----------|-----------|
|
||||
| Protocol | Standard WS | Custom on top of WS |
|
||||
| Reconnection | Manual | Automatic |
|
||||
| Rooms/Namespaces | No | Yes |
|
||||
| Fallback | None | HTTP long-polling |
|
||||
| Binary support | Yes | Yes |
|
||||
|
||||
**When to use raw WebSocket:**
|
||||
- You need maximum performance
|
||||
- You're building a simple real-time feature
|
||||
- You want to keep dependencies minimal
|
||||
|
||||
**When to use Socket.IO:**
|
||||
- You need automatic reconnection
|
||||
- You want room-based broadcasting
|
||||
- You need to support older browsers`,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
role: "user",
|
||||
content:
|
||||
"Can you show me how to set up a simple Socket.IO server with event handling?",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
role: "assistant",
|
||||
content: `Sure! Here's a minimal Socket.IO server:
|
||||
|
||||
\`\`\`ts
|
||||
import { Server } from "socket.io";
|
||||
|
||||
const io = new Server(3000, {
|
||||
cors: { origin: "*" },
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log(\`Client connected: \${socket.id}\`);
|
||||
|
||||
socket.on("message", (data) => {
|
||||
// Broadcast to all other clients
|
||||
socket.broadcast.emit("message", {
|
||||
from: socket.id,
|
||||
...data,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log(\`Client disconnected: \${socket.id}\`);
|
||||
});
|
||||
});
|
||||
\`\`\`
|
||||
|
||||
And the client side:
|
||||
|
||||
\`\`\`ts
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
const socket = io("http://localhost:3000");
|
||||
|
||||
socket.on("connect", () => {
|
||||
socket.emit("message", { text: "Hello everyone!" });
|
||||
});
|
||||
|
||||
socket.on("message", (data) => {
|
||||
console.log("New message:", data);
|
||||
});
|
||||
\`\`\`
|
||||
|
||||
That's it — the server handles connections, relays messages, and both sides can emit custom events freely.`,
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
role: "user",
|
||||
content: "Thanks! How would I add authentication to this?",
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
role: "assistant",
|
||||
content: `You can use Socket.IO's **middleware** to authenticate on connection:
|
||||
|
||||
\`\`\`ts
|
||||
io.use((socket, next) => {
|
||||
const token = socket.handshake.auth.token;
|
||||
|
||||
try {
|
||||
const user = verifyJWT(token);
|
||||
socket.data.user = user;
|
||||
next();
|
||||
} catch {
|
||||
next(new Error("Authentication failed"));
|
||||
}
|
||||
});
|
||||
\`\`\`
|
||||
|
||||
On the client, pass the token when connecting:
|
||||
|
||||
\`\`\`ts
|
||||
const socket = io("http://localhost:3000", {
|
||||
auth: {
|
||||
token: "your-jwt-token-here",
|
||||
},
|
||||
});
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
console.error("Auth failed:", err.message);
|
||||
});
|
||||
\`\`\`
|
||||
|
||||
Key points:
|
||||
- The middleware runs **before** the \`connection\` event fires
|
||||
- You can attach user data to \`socket.data\` for later use
|
||||
- Failed auth rejects the connection entirely — no messages leak`,
|
||||
},
|
||||
];
|
||||
|
||||
export function useMessages(): Message[] {
|
||||
return MOCK_MESSAGES;
|
||||
export interface Message {
|
||||
id: string
|
||||
role: "user" | "assistant"
|
||||
content: string
|
||||
agentId: string
|
||||
}
|
||||
|
||||
export function useMessages() {
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
|
||||
const addUserMessage = useCallback((content: string, agentId: string) => {
|
||||
setMessages(prev => [...prev, { id: uuidv7(), role: "user", content, agentId }])
|
||||
}, [])
|
||||
|
||||
const addAssistantMessage = useCallback((content: string, agentId: string) => {
|
||||
setMessages(prev => [...prev, { id: uuidv7(), role: "assistant", content, agentId }])
|
||||
}, [])
|
||||
|
||||
const clearMessages = useCallback(() => setMessages([]), [])
|
||||
|
||||
return { messages, addUserMessage, addAssistantMessage, clearMessages }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue