From f62c40041bca98a21ad1a61b3718e67466669c27 Mon Sep 17 00:00:00 2001 From: JianMiau Date: Mon, 20 Jul 2026 15:19:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=8D=E8=A9=B1=E5=8F=AF=E5=88=87=E5=85=A8?= =?UTF-8?q?=E8=9E=A2=E5=B9=95,=E4=BA=A6=E5=8F=AF=E7=B8=AE=E5=9B=9E?= =?UTF-8?q?=E5=81=B4=E9=82=8A=E9=9D=A2=E6=9D=BF=E5=9B=9E=E5=88=B0=E8=BE=A6?= =?UTF-8?q?=E5=85=AC=E5=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 摘要: 與 claude-office 同步:LIVE 對話新增全螢幕切換,展開為置中聊天卡片, 可用「回到辦公室」鈕或 Esc 縮回。 根本原因: 對話擠在側欄不利長對話閱讀。 影響: 純 UI 覆蓋層;側欄與全螢幕共用 ChatConversation,狀態同步。 修法: 抽出 ChatConversation(variant panel|full)、ChatFullscreen 覆蓋層、 store chatFullscreen 狀態、全螢幕毛玻璃版面。 Co-Authored-By: Claude Fable 5 --- src/App.tsx | 59 ++++++++------- src/components/ChatConversation.tsx | 109 ++++++++++++++++++++++++++++ src/components/ChatFullscreen.tsx | 54 ++++++++++++++ src/components/SidePanel.tsx | 92 +---------------------- src/store/office-store.ts | 8 +- src/styles.css | 83 ++++++++++++++++++++- 6 files changed, 289 insertions(+), 116 deletions(-) create mode 100644 src/components/ChatConversation.tsx create mode 100644 src/components/ChatFullscreen.tsx 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 &&
⋯ 執行中,過程看辦公室裡的小人
} +
+ )} + +