Files
claude-office/src/components/ChatConversation.tsx
T
JianMiauandClaude Fable 5 aa6a8b8242 對話可切全螢幕,亦可縮回側邊面板回到辦公室
摘要:
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 <noreply@anthropic.com>
2026-07-20 15:19:26 +08:00

110 lines
3.5 KiB
TypeScript

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<HTMLDivElement>(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 (
<div className={`chat-panel ${variant === "full" ? "chat-panel-full" : ""}`}>
<div className="chat-head">
<h3>對話</h3>
<div className="chat-head-actions">
{started && (
<button className="link-btn" onClick={() => resetChat(agentId)} disabled={busy}>
🔄 新對話
</button>
)}
<button
className="link-btn"
onClick={() => setChatFullscreen(!isFull)}
title={isFull ? "縮回側邊面板" : "全螢幕對話"}
>
{isFull ? "⊟ 縮小" : "⛶ 全螢幕"}
</button>
</div>
</div>
{started && (
<div className="chat-list" ref={listRef}>
{messages.map((m, i) => (
<div key={i} className={`chat-msg ${m.role}`}>
{m.text}
</div>
))}
{busy && <div className="chat-msg pending"> 執行中,過程看辦公室裡的小人</div>}
</div>
)}
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={started ? "繼續對話…(Enter 送出)" : "跟這個專案的 Claude 對話…(Enter 送出)"}
rows={variant === "full" ? 3 : 2}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
send();
}
}}
/>
<div className="task-composer-row">
{!started ? (
<label
className="tip"
data-tip="第一句話從該專案最近一段「已結束」的 session 接續(--resume)。注意:無法插入正在終端機進行中的互動對話。"
>
<input
type="checkbox"
checked={fromProject}
onChange={(e) => setFromProject(e.target.checked)}
/>
接續專案最近 session
</label>
) : (
<span
className="chat-hint tip"
data-tip="辦公室對話有自己的 session,每則訊息自動以 --resume 延續,像 claudecodeui 一樣逐則接力。"
>
對話自動延續
</span>
)}
<button className="btn assign-btn" onClick={send} disabled={busy || !input.trim()}>
{busy ? "執行中…" : "送出"}
</button>
</div>
</div>
);
}