diff --git a/src/App.tsx b/src/App.tsx index 37c1ebd..0277717 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,33 @@ -import { useEffect } from "react"; -import { FloorPlan } from "./components/FloorPlan"; -import { HeaderBar } from "./components/HeaderBar"; -import { SidePanel } from "./components/SidePanel"; -import { setMode, startRuntime } from "./sim/runtime"; -import { loadPreferredMode, useOfficeStore } from "./store/office-store"; - -export default function App() { - const theme = useOfficeStore((s) => s.theme); - const panelOpen = useOfficeStore((s) => s.panelOpen); - - useEffect(() => { - startRuntime(); - if (loadPreferredMode() === "live") setMode("live"); - }, []); - - return ( -
- -
- - {panelOpen && } -
-
- ); -} +import { useEffect } from "react"; +import { ChatFullscreen } from "./components/ChatFullscreen"; +import { FloorPlan } from "./components/FloorPlan"; +import { HeaderBar } from "./components/HeaderBar"; +import { SidePanel } from "./components/SidePanel"; +import { setMode, startRuntime } from "./sim/runtime"; +import { loadPreferredMode, useOfficeStore } from "./store/office-store"; + +export default function App() { + const theme = useOfficeStore((s) => s.theme); + const panelOpen = useOfficeStore((s) => s.panelOpen); + const chatFullscreen = useOfficeStore((s) => s.chatFullscreen); + const selectedAgentId = useOfficeStore((s) => s.selectedAgentId); + const mode = useOfficeStore((s) => s.mode); + + useEffect(() => { + startRuntime(); + if (loadPreferredMode() === "live") setMode("live"); + }, []); + + const showFullscreenChat = chatFullscreen && mode === "live" && selectedAgentId !== null; + + return ( +
+ +
+ + {panelOpen && } +
+ {showFullscreenChat && } +
+ ); +} diff --git a/src/components/ChatConversation.tsx b/src/components/ChatConversation.tsx new file mode 100644 index 0000000..0c1c9a0 --- /dev/null +++ b/src/components/ChatConversation.tsx @@ -0,0 +1,109 @@ +import { useEffect, useRef, useState } from "react"; +import { useOfficeStore } from "@/store/office-store"; +import { resetChat, sendChat } from "@/gateway/live"; + +/** + * Two-way chat with one project — each message runs headless and is stitched + * to the previous with --resume. Shared by the side panel and the full-screen + * overlay via the `variant` prop. + */ +export function ChatConversation({ + agentId, + variant, +}: { + agentId: string; + variant: "panel" | "full"; +}) { + const thread = useOfficeStore((s) => s.chats.get(agentId)); + const setChatFullscreen = useOfficeStore((s) => s.setChatFullscreen); + const isFull = useOfficeStore((s) => s.chatFullscreen); + const [input, setInput] = useState(""); + const [fromProject, setFromProject] = useState(false); + const listRef = useRef(null); + + const busy = thread?.busy ?? false; + const messages = thread?.messages ?? []; + const started = messages.length > 0; + + useEffect(() => { + listRef.current?.scrollTo({ top: listRef.current.scrollHeight }); + }, [messages.length, busy]); + + const send = () => { + const text = input.trim(); + if (!text || busy) return; + sendChat(agentId, text, fromProject); + setInput(""); + }; + + return ( +
+
+

對話

+
+ {started && ( + + )} + +
+
+ + {started && ( +
+ {messages.map((m, i) => ( +
+ {m.text} +
+ ))} + {busy &&
⋯ 執行中,過程看辦公室裡的小人
} +
+ )} + +