From aa6a8b8242511638fd2c63265b6277fc8c27061b Mon Sep 17 00:00:00 2001 From: JianMiau Date: Mon, 20 Jul 2026 15:19:26 +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 摘要: LIVE 模式對話新增「⛶ 全螢幕」按鈕,展開為置中的全螢幕聊天卡片 (辦公室模糊在後);全螢幕內有「⊟ 回到辦公室」鈕、對話列有「縮小」, 按 Esc 亦可返回。 根本原因: 對話擠在 300px 側欄裡,長對話讀起來侷促,需要一個能專心對話的大畫面, 且要能隨時縮回辦公室繼續看小人。 影響: 新增純 UI 覆蓋層,不影響資料流;側欄與全螢幕共用同一對話元件, 狀態同步(同一 store thread)。取消選取或切換模式時自動關閉全螢幕。 修法: - 抽出 ChatConversation 共用元件(variant: panel | full) - ChatFullscreen 覆蓋層(頂欄 agent 資訊 + Esc 關閉) - store 新增 chatFullscreen / setChatFullscreen;selectAgent(null) 與 clearWorld 時歸零;App 依 chatFullscreen && live && 已選取 渲染覆蓋層 - styles:.chat-full-overlay 毛玻璃背景、卡片版面、full variant 撐滿高度 Co-Authored-By: Claude Fable 5 --- src/App.tsx | 7 ++ src/components/ChatConversation.tsx | 109 ++++++++++++++++++++++++++++ src/components/ChatFullscreen.tsx | 54 ++++++++++++++ src/components/SidePanel.tsx | 92 +---------------------- src/store/office-store.ts | 8 +- src/styles.css | 86 ++++++++++++++++++++++ 6 files changed, 267 insertions(+), 89 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..bfb98a8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { useEffect } from "react"; +import { ChatFullscreen } from "./components/ChatFullscreen"; import { FloorPlan } from "./components/FloorPlan"; import { HeaderBar } from "./components/HeaderBar"; import { SidePanel } from "./components/SidePanel"; @@ -8,12 +9,17 @@ 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 (
@@ -21,6 +27,7 @@ export default function App() { {panelOpen && }
+ {showFullscreenChat && } ); } diff --git a/src/components/ChatConversation.tsx b/src/components/ChatConversation.tsx new file mode 100644 index 0000000..22459ed --- /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 &&
⋯ 執行中,過程看辦公室裡的小人
} +
+ )} + +