refactor(frontend): complete migration of all hooks and components to packages
- Create gateway store in @multica/store (WS connection independent of components) - Gateway auto-connects when hub is ready, messages handled internally - Move scroll-fade hook to @multica/ui/hooks - Move Chat component to @multica/ui/components - Add setConfig() call in web layout for URL injection - Delete all web-local hooks, components, and lib/config - Web app is now a pure shell: layout.tsx + page.tsx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
97fce5b113
commit
eef26d8675
12 changed files with 88 additions and 60 deletions
|
|
@ -1,44 +0,0 @@
|
|||
import { useEffect, useRef, useState, useCallback } from "react"
|
||||
import { GatewayClient, type ConnectionState, type RoutedMessage } from "@multica/sdk"
|
||||
import { useDeviceId } from "@multica/store"
|
||||
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)
|
||||
|
||||
useEffect(() => {
|
||||
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 }
|
||||
}
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono, Inter, Playfair_Display } from "next/font/google";
|
||||
import { setConfig } from "@multica/fetch";
|
||||
import "@multica/ui/globals.css";
|
||||
|
||||
setConfig({
|
||||
consoleUrl: process.env.NEXT_PUBLIC_CONSOLE_URL ?? "http://localhost:4000",
|
||||
gatewayUrl: process.env.NEXT_PUBLIC_GATEWAY_URL ?? "http://localhost:3000",
|
||||
});
|
||||
import {
|
||||
SidebarProvider,
|
||||
SidebarInset,
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
export const GATEWAY_URL = process.env.NEXT_PUBLIC_GATEWAY_URL ?? "http://localhost:3000"
|
||||
export const CONSOLE_URL = process.env.NEXT_PUBLIC_CONSOLE_URL ?? "http://localhost:4000"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Chat } from "./components/chat";
|
||||
import { Chat } from "@multica/ui/components/chat";
|
||||
|
||||
export default function Page() {
|
||||
return <Chat />;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@multica/fetch": "workspace:*",
|
||||
"@multica/sdk": "workspace:*",
|
||||
"@multica/store": "workspace:*",
|
||||
"@multica/ui": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@multica/fetch": "workspace:*",
|
||||
"@multica/sdk": "workspace:*",
|
||||
"react": "catalog:",
|
||||
"uuid": "^13.0.0",
|
||||
"zustand": "catalog:"
|
||||
|
|
|
|||
57
packages/store/src/gateway.ts
Normal file
57
packages/store/src/gateway.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { create } from "zustand"
|
||||
import { GatewayClient, type ConnectionState, type SendErrorResponse } from "@multica/sdk"
|
||||
import { getGatewayUrl } from "@multica/fetch"
|
||||
import { useMessagesStore } from "./messages"
|
||||
|
||||
interface GatewayState {
|
||||
connectionState: ConnectionState
|
||||
lastError: SendErrorResponse | null
|
||||
}
|
||||
|
||||
interface GatewayActions {
|
||||
connect: (deviceId: string) => void
|
||||
disconnect: () => void
|
||||
send: (to: string, action: string, payload: unknown) => void
|
||||
}
|
||||
|
||||
export type GatewayStore = GatewayState & GatewayActions
|
||||
|
||||
let client: GatewayClient | null = null
|
||||
|
||||
export const useGatewayStore = create<GatewayStore>()((set) => ({
|
||||
connectionState: "disconnected",
|
||||
lastError: null,
|
||||
|
||||
connect: (deviceId) => {
|
||||
if (client) return
|
||||
|
||||
client = new GatewayClient({
|
||||
url: getGatewayUrl(),
|
||||
deviceId,
|
||||
deviceType: "client",
|
||||
})
|
||||
.onStateChange((connectionState) => set({ connectionState }))
|
||||
.onMessage((msg) => {
|
||||
const payload = msg.payload as { agentId?: string; content?: string }
|
||||
if (payload?.agentId && payload?.content) {
|
||||
useMessagesStore.getState().addAssistantMessage(payload.content, payload.agentId)
|
||||
}
|
||||
})
|
||||
.onSendError((error) => set({ lastError: error }))
|
||||
|
||||
client.connect()
|
||||
},
|
||||
|
||||
disconnect: () => {
|
||||
if (client) {
|
||||
client.disconnect()
|
||||
client = null
|
||||
}
|
||||
set({ connectionState: "disconnected" })
|
||||
},
|
||||
|
||||
send: (to, action, payload) => {
|
||||
if (!client?.isRegistered) return
|
||||
client.send(to, action, payload)
|
||||
},
|
||||
}))
|
||||
|
|
@ -2,11 +2,14 @@
|
|||
|
||||
import { useEffect } from "react"
|
||||
import { useHubStore } from "./hub"
|
||||
import { useDeviceId } from "./device-id"
|
||||
import { useGatewayStore } from "./gateway"
|
||||
|
||||
export function useHubInit() {
|
||||
const fetchHub = useHubStore((s) => s.fetchHub)
|
||||
const status = useHubStore((s) => s.status)
|
||||
const fetchAgents = useHubStore((s) => s.fetchAgents)
|
||||
const deviceId = useDeviceId()
|
||||
|
||||
useEffect(() => { fetchHub() }, [fetchHub])
|
||||
useEffect(() => {
|
||||
|
|
@ -16,4 +19,11 @@ export function useHubInit() {
|
|||
const id = setInterval(fetchHub, 30_000)
|
||||
return () => clearInterval(id)
|
||||
}, [fetchHub])
|
||||
|
||||
// Connect gateway when hub is ready and deviceId is available
|
||||
useEffect(() => {
|
||||
if (status === "connected" && deviceId) {
|
||||
useGatewayStore.getState().connect(deviceId)
|
||||
}
|
||||
}, [status, deviceId])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,3 +4,5 @@ export { useHubInit } from "./hub-init"
|
|||
export { useDeviceId } from "./device-id"
|
||||
export { useMessagesStore } from "./messages"
|
||||
export type { Message, MessagesStore } from "./messages"
|
||||
export { useGatewayStore } from "./gateway"
|
||||
export type { GatewayStore } from "./gateway"
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@ import { MemoizedMarkdown } from "@multica/ui/components/markdown";
|
|||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { UserIcon, Copy01Icon, CheckmarkCircle02Icon } from "@hugeicons/core-free-icons";
|
||||
import { toast } from "@multica/ui/components/ui/sonner";
|
||||
import { useGateway } from "../hooks/use-gateway";
|
||||
import { useHubStore, useDeviceId, useMessagesStore } from "@multica/store";
|
||||
import { useScrollFade } from "../hooks/use-scroll-fade";
|
||||
import { useHubStore, useDeviceId, useMessagesStore, useGatewayStore } from "@multica/store";
|
||||
import { useScrollFade } from "@multica/ui/hooks/use-scroll-fade";
|
||||
import { cn } from "@multica/ui/lib/utils";
|
||||
|
||||
const STATE_VARIANT: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
|
||||
|
|
@ -25,17 +24,9 @@ export function Chat() {
|
|||
const activeAgentId = useHubStore((s) => s.activeAgentId)
|
||||
const hub = useHubStore((s) => s.hub)
|
||||
const addUserMessage = useMessagesStore((s) => s.addUserMessage)
|
||||
const addAssistantMessage = useMessagesStore((s) => s.addAssistantMessage)
|
||||
const messages = useMessagesStore((s) => s.messages)
|
||||
|
||||
const { state: gwState, send } = useGateway({
|
||||
onMessage: (msg) => {
|
||||
const payload = msg.payload as { agentId?: string; content?: string }
|
||||
if (payload?.agentId && payload?.content) {
|
||||
addAssistantMessage(payload.content, payload.agentId)
|
||||
}
|
||||
},
|
||||
})
|
||||
const gwState = useGatewayStore((s) => s.connectionState)
|
||||
const send = useGatewayStore((s) => s.send)
|
||||
|
||||
const handleSend = (text: string) => {
|
||||
if (!hub?.hubId || !activeAgentId) return
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
|
|
@ -211,6 +211,9 @@ importers:
|
|||
'@hugeicons/react':
|
||||
specifier: ^1.1.4
|
||||
version: 1.1.4(react@19.2.3)
|
||||
'@multica/fetch':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/fetch
|
||||
'@multica/sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/sdk
|
||||
|
|
@ -282,6 +285,9 @@ importers:
|
|||
'@multica/fetch':
|
||||
specifier: workspace:*
|
||||
version: link:../fetch
|
||||
'@multica/sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../sdk
|
||||
react:
|
||||
specifier: 'catalog:'
|
||||
version: 19.2.3
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue