- Extract ConnectPrompt: self-contained connection code input - Extract MessageList: pure display of messages with streaming support - Add ChatSkeleton: skeleton placeholder shown during reconnection - Chat component: three-state rendering (skeleton → connect → messages), keeps <main ref> always mounted so useScrollFade works correctly - Remove hub-sidebar.tsx (dead code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
78 lines
2.7 KiB
TypeScript
78 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useCallback } from "react";
|
|
import { Button } from "@multica/ui/components/ui/button";
|
|
import { ChatInput } from "@multica/ui/components/chat-input";
|
|
import { useConnectionStore, useMessagesStore, useAutoConnect } from "@multica/store";
|
|
import { useScrollFade } from "@multica/ui/hooks/use-scroll-fade";
|
|
import { useAutoScroll } from "@multica/ui/hooks/use-auto-scroll";
|
|
import { ConnectPrompt } from "./connect-prompt";
|
|
import { MessageList } from "./message-list";
|
|
import { ChatSkeleton } from "./chat-skeleton";
|
|
|
|
export function Chat() {
|
|
const { loading } = useAutoConnect()
|
|
|
|
const agentId = useConnectionStore((s) => s.agentId)
|
|
const gwState = useConnectionStore((s) => s.connectionState)
|
|
const hubId = useConnectionStore((s) => s.hubId)
|
|
|
|
const messages = useMessagesStore((s) => s.messages)
|
|
const streamingIds = useMessagesStore((s) => s.streamingIds)
|
|
|
|
const isConnected = gwState === "registered" && !!hubId && !!agentId
|
|
|
|
const handleSend = useCallback((text: string) => {
|
|
const { hubId, agentId, send, connectionState } = useConnectionStore.getState()
|
|
if (connectionState !== "registered" || !hubId || !agentId) return
|
|
useMessagesStore.getState().sendMessage(text, { hubId, agentId, send })
|
|
}, [])
|
|
|
|
const handleDisconnect = useCallback(() => {
|
|
useConnectionStore.getState().disconnect()
|
|
}, [])
|
|
|
|
const mainRef = useRef<HTMLElement>(null)
|
|
const fadeStyle = useScrollFade(mainRef)
|
|
useAutoScroll(mainRef)
|
|
|
|
return (
|
|
<div className="h-full flex flex-col overflow-hidden w-full">
|
|
{isConnected && (
|
|
<div className="flex items-center justify-end px-4 py-1 max-w-4xl mx-auto w-full">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleDisconnect}
|
|
className="text-xs text-muted-foreground"
|
|
>
|
|
Disconnect
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<main ref={mainRef} className="flex-1 overflow-y-auto min-h-0" style={fadeStyle}>
|
|
{loading ? (
|
|
<ChatSkeleton />
|
|
) : !isConnected ? (
|
|
<ConnectPrompt />
|
|
) : messages.length === 0 ? (
|
|
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
Send a message to start the conversation
|
|
</div>
|
|
) : (
|
|
<MessageList messages={messages} streamingIds={streamingIds} />
|
|
)}
|
|
</main>
|
|
|
|
{/* Footer */}
|
|
<footer className="w-full p-2 pt-1 max-w-4xl mx-auto">
|
|
<ChatInput
|
|
onSubmit={handleSend}
|
|
disabled={!isConnected}
|
|
placeholder={!isConnected ? "Connect first..." : "Type a message..."}
|
|
/>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|