import { useEffect, useMemo, useRef } from "react"; import { useAppStore } from "../store/useAppStore"; import { useModeStore } from "../store/useModeStore"; import { useSendChat } from "../lib/useChat"; import { sendUp } from "../lib/ws"; import { MessageBubble } from "../components/chat"; import { ChatInput } from "../components/ChatInput"; import { EmptyState } from "../components/primitives"; import { Icon } from "../components/Icon"; import { SessionStatusHint } from "../components/status"; /** * The single shared page (design/07 §1). All behavior modes live in ONE message * stream; each message carries its own `mode` (assigned by the agent at runtime, * design/01 §3 hot-switch), so its bubble renders per that mode. There is no * manual mode selector — the agent / backend drives the mode. */ export function ChatShell(): React.ReactNode { const messages = useAppStore((s) => s.messages); const { send, sending } = useSendChat(); const bottomRef = useRef(null); // The latest assistant message is the one being streamed right now; only it // carries the live 唤醒路径 node sequence (WakePath), so the capacity state // reflects the current turn rather than a collapsed historical message. const lastAssistantId = useMemo(() => { for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].role === "assistant") return messages[i].id; } return undefined; }, [messages]); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); }, [messages]); return (
{messages.length === 0 ? ( } title="开始与昔涟对话" hint="模式只改变消息框的呈现方式,会话上下文与记忆保持不变" /> ) : ( messages.map((m) => ( )) )}
void send(t, image)} sending={sending} onVoice={() => { // Voice is not wired to a live pipeline yet (real call deferred). Activate // the voice_input uplink and switch the shell to voice mode so the agent // (or the demo) reflects a voice session. useModeStore.getState().setMode("voice"); sendUp({ type: "voice_input", data: { mode: "voice" } }); }} />
); }