refactor(ui): purify Chat component and move header to app layouts

- Remove all props from Chat (showHeader, headerActions) making it a
  zero-config pure chat component with only connection input, messages,
  and send functionality
- Create AppHeader client component for web app with brand, theme
  toggle, disconnect button, and useHubInit
- Add disconnect button to desktop layout header
- Add reset() action to hub store to eliminate duplicated state reset
- Remove unused token field from gateway store
- Remove dead code: connection-bar.tsx
- Guard handleConnect against empty deviceId race condition

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-02-03 20:31:24 +08:00
parent b6d7626239
commit dacbfa9e3d
10 changed files with 106 additions and 186 deletions

View file

@ -11,22 +11,15 @@ import {
useHubStore,
useMessagesStore,
useGatewayStore,
useHubInit,
useDeviceId,
parseConnectionCode,
saveConnection,
clearConnection,
} from "@multica/store";
import { useScrollFade } from "@multica/ui/hooks/use-scroll-fade";
import { useAutoScroll } from "@multica/ui/hooks/use-auto-scroll";
import { cn } from "@multica/ui/lib/utils";
interface ChatProps {
showHeader?: boolean;
}
export function Chat({ showHeader = true }: ChatProps) {
useHubInit()
export function Chat() {
const deviceId = useDeviceId()
const activeAgentId = useHubStore((s) => s.activeAgentId)
const gwState = useGatewayStore((s) => s.connectionState)
@ -41,7 +34,7 @@ export function Chat({ showHeader = true }: ChatProps) {
const handleConnect = useCallback(() => {
const trimmed = codeInput.trim()
if (!trimmed) return
if (!trimmed || !deviceId) return
try {
const info = parseConnectionCode(trimmed)
saveConnection(info)
@ -52,12 +45,6 @@ export function Chat({ showHeader = true }: ChatProps) {
}
}, [codeInput, deviceId])
const handleDisconnect = useCallback(() => {
useGatewayStore.getState().disconnect()
useHubStore.setState({ status: "idle", hub: null, agents: [], activeAgentId: null })
clearConnection()
}, [])
const handleSend = useCallback((text: string) => {
const { hubId } = useGatewayStore.getState()
const agentId = useHubStore.getState().activeAgentId
@ -72,33 +59,6 @@ export function Chat({ showHeader = true }: ChatProps) {
return (
<div className="h-full flex flex-col overflow-hidden w-full">
{/* Header */}
{showHeader && (
<header>
<div className="flex items-center justify-between px-4 py-2 max-w-4xl mx-auto">
<div className="flex items-center gap-2.5">
<img src="/icon.png" alt="Multica" className="size-6 rounded-md" />
<span className="text-sm tracking-wide font-[family-name:var(--font-brand)]">
Multica
</span>
</div>
<div className="flex items-center gap-1">
{isConnected && (
<Button
variant="ghost"
size="sm"
onClick={handleDisconnect}
className="text-xs text-muted-foreground"
>
Disconnect
</Button>
)}
</div>
</div>
</header>
)}
{/* Main */}
<main ref={mainRef} className="flex-1 overflow-y-auto min-h-0" style={fadeStyle}>
{!isConnected ? (
<div className="flex flex-col items-center justify-center h-full gap-4 px-4">

View file

@ -1,119 +0,0 @@
"use client"
import { useState } from "react"
import { Button } from "@multica/ui/components/ui/button"
import { Textarea } from "@multica/ui/components/ui/textarea"
import { toast } from "@multica/ui/components/ui/sonner"
import {
useGatewayStore,
useHubStore,
useDeviceId,
useHubInit,
parseConnectionCode,
saveConnection,
clearConnection,
} from "@multica/store"
const STATUS_DOT: Record<string, string> = {
registered: "bg-green-500",
connected: "bg-yellow-500 animate-pulse",
connecting: "bg-yellow-500 animate-pulse",
disconnected: "bg-red-500",
}
export function ConnectionBar() {
useHubInit()
const deviceId = useDeviceId()
const gwState = useGatewayStore((s) => s.connectionState)
const hubId = useGatewayStore((s) => s.hubId)
const agentId = useGatewayStore((s) => s.agentId)
const hubStatus = useHubStore((s) => s.status)
const isConnected = gwState === "registered" && hubId
const [codeInput, setCodeInput] = useState("")
const handleConnect = () => {
const trimmed = codeInput.trim()
if (!trimmed) return
try {
const info = parseConnectionCode(trimmed)
saveConnection(info)
useGatewayStore.getState().connectWithCode(info, deviceId)
setCodeInput("")
} catch (e) {
toast.error((e as Error).message)
}
}
const handleDisconnect = () => {
useGatewayStore.getState().disconnect()
useHubStore.setState({ status: "idle", hub: null, agents: [], activeAgentId: null })
clearConnection()
}
return (
<div className="w-64 shrink-0 border-r flex flex-col h-dvh">
<div className="flex items-center gap-2.5 p-4 pb-2">
<img src="/icon.png" alt="Multica" className="size-7 rounded-md" />
<span className="text-sm tracking-wide font-[family-name:var(--font-brand)]">
Multica
</span>
</div>
<div className="flex-1 p-4 pt-2">
{isConnected ? (
<div className="space-y-3">
<div className="flex items-center gap-2">
<span className={`size-2 rounded-full shrink-0 ${STATUS_DOT[gwState]}`} />
<span className="text-xs text-muted-foreground">
{hubStatus === "connected" ? "Connected" : "Connecting..."}
</span>
</div>
<div className="space-y-1 text-xs text-muted-foreground/70 font-mono">
<div className="truncate" title={hubId}>Hub: {hubId}</div>
{agentId && <div className="truncate" title={agentId}>Agent: {agentId}</div>}
</div>
<Button
variant="outline"
size="sm"
onClick={handleDisconnect}
className="w-full text-xs"
>
Disconnect
</Button>
</div>
) : gwState === "connecting" || gwState === "connected" ? (
<div className="flex items-center gap-2">
<span className="size-2 rounded-full shrink-0 bg-yellow-500 animate-pulse" />
<span className="text-xs text-muted-foreground">Connecting...</span>
</div>
) : (
<div className="space-y-3">
<Textarea
value={codeInput}
onChange={(e) => setCodeInput(e.target.value)}
placeholder="Paste connection code..."
className="text-xs font-mono min-h-[80px] resize-none"
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
handleConnect()
}
}}
/>
<Button
variant="outline"
size="sm"
onClick={handleConnect}
disabled={!codeInput.trim()}
className="w-full text-xs"
>
Connect
</Button>
</div>
)}
</div>
</div>
)
}