Unverified 提交 403e4308 authored 作者: Adekunle James Adeniji's avatar Adekunle James Adeniji 提交者: GitHub

Add chat panel visibility toggle functionality (#2345)

Closes #2140 <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2345"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Implements a chat panel visibility toggle and syncs it with the resizable layout. > > - Adds `isChatPanelHiddenAtom` to manage chat panel visibility state > - Adds a header toggle in `PreviewIframe` (with tooltip, icons, and `data-testid`) to hide/show the chat panel > - Makes `chat` page's `Panel` for `#chat-panel` collapsible; wires refs to expand/collapse and syncs with the atom; preserves preview panel behavior and resize handle > - Adds Playwright e2e test `e2e-tests/chat_panel_toggle.spec.ts` verifying collapse/expand behavior > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d27fd58182f7012ad2b07fb3d1584a07b584bf45. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds a toolbar toggle to hide and show the chat panel, helping users focus on the preview when needed. The chat panel is now collapsible and synced with app state and the resize handle, with an e2e test covering the flow. - New Features - Preview toolbar button toggles chat visibility (Maximize2/Minimize2 with tooltip). - Added isChatPanelHiddenAtom; syncs panel size with hidden state, restores previous size on re-open, and updates visibility when the handle is dragged (hidden below ~5% width). - Playwright test confirms #chat-panel hides/shows via data-testid="preview-toggle-chat-panel-button". <sup>Written for commit f41058bfec814dde3d70a53cf20792e9d997f217. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 ec2807f0
import { testSkipIfWindows } from "./helpers/test_helper";
import { expect } from "@playwright/test";
testSkipIfWindows("toggle chat panel visibility", async ({ po }) => {
await po.setUp({ autoApprove: true });
// We are in the chat view after setUp
await po.sendPrompt("basic");
// Chat panel should be visible initially.
const chatPanel = po.page.locator("#chat-panel");
await expect(chatPanel).toBeVisible();
// Toggle button
const toggleButton = po.page.getByTestId("preview-toggle-chat-panel-button");
// Collapse
await toggleButton.click();
await expect(chatPanel).toBeHidden();
// Expand
await toggleButton.click();
// Expect chat panel to be visible
await expect(chatPanel).toBeVisible();
});
......@@ -2,6 +2,7 @@ import { atom } from "jotai";
import { SECTION_IDS } from "@/lib/settingsSearchIndex";
export const isPreviewOpenAtom = atom(true);
export const isChatPanelHiddenAtom = atom(false);
export const selectedFileAtom = atom<{
path: string;
line?: number | null;
......
......@@ -25,6 +25,8 @@ import {
Tablet,
Smartphone,
Pen,
Maximize2,
Minimize2,
} from "lucide-react";
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
import { CopyErrorMessage } from "@/components/CopyErrorMessage";
......@@ -47,6 +49,7 @@ import {
screenshotDataUrlAtom,
pendingVisualChangesAtom,
} from "@/atoms/previewAtoms";
import { isChatPanelHiddenAtom } from "@/atoms/viewAtoms";
import { ComponentSelection } from "@/ipc/types";
import {
Popover,
......@@ -220,6 +223,9 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
const [screenshotDataUrl, setScreenshotDataUrl] = useAtom(
screenshotDataUrlAtom,
);
const [isChatPanelHidden, setIsChatPanelHidden] = useAtom(
isChatPanelHiddenAtom,
);
const { addAttachments } = useAttachments();
const setPendingChanges = useSetAtom(pendingVisualChangesAtom);
......@@ -983,6 +989,18 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
<div className="flex items-center p-2 border-b space-x-2">
{/* Navigation Buttons */}
<div className="flex space-x-1">
<button
onClick={() => setIsChatPanelHidden(!isChatPanelHidden)}
className="p-1 rounded transition-colors duration-200 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
data-testid="preview-toggle-chat-panel-button"
title={isChatPanelHidden ? "Show chat" : "Hide chat"}
>
{isChatPanelHidden ? (
<Maximize2 size={16} />
) : (
<Minimize2 size={16} />
)}
</button>
<button
onClick={handleActivateComponentSelector}
className={`p-1 rounded transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed ${
......
......@@ -10,18 +10,25 @@ import { PreviewPanel } from "../components/preview_panel/PreviewPanel";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { cn } from "@/lib/utils";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
import { isPreviewOpenAtom, isChatPanelHiddenAtom } from "@/atoms/viewAtoms";
import { useChats } from "@/hooks/useChats";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
const DEFAULT_CHAT_PANEL_SIZE = 50;
export default function ChatPage() {
let { id: chatId } = useSearch({ from: "/chat" });
const { id: chatId } = useSearch({ from: "/chat" });
const navigate = useNavigate();
const [isPreviewOpen, setIsPreviewOpen] = useAtom(isPreviewOpenAtom);
const [isChatPanelHidden, setIsChatPanelHidden] = useAtom(
isChatPanelHiddenAtom,
);
const [isResizing, setIsResizing] = useState(false);
const selectedAppId = useAtomValue(selectedAppIdAtom);
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
const { chats, loading } = useChats(selectedAppId);
const previousSizeRef = useRef<number>(DEFAULT_CHAT_PANEL_SIZE);
const isInitialMountRef = useRef(true);
useEffect(() => {
if (!chatId && chats.length && !loading) {
......@@ -40,43 +47,90 @@ export default function ChatPage() {
}
}, [isPreviewOpen]);
const ref = useRef<ImperativePanelHandle>(null);
const chatPanelRef = useRef<ImperativePanelHandle>(null);
// Keep chat panel size in sync with hidden state (from toolbar button / other views)
useEffect(() => {
if (!chatPanelRef.current) return;
// Skip the initial mount to preserve persisted panel size from autoSaveId
if (isInitialMountRef.current) {
isInitialMountRef.current = false;
return;
}
if (isChatPanelHidden) {
// Save current size before collapsing
const currentSize = chatPanelRef.current.getSize();
if (currentSize > 5) {
previousSizeRef.current = currentSize;
}
// Visually collapsed but keep a sliver so the handle is usable
chatPanelRef.current.resize(1);
} else {
// Restore to previous size when re-opened via button
chatPanelRef.current.resize(previousSizeRef.current);
}
}, [isChatPanelHidden]);
return (
<PanelGroup autoSaveId="persistence" direction="horizontal">
<Panel id="chat-panel" minSize={30}>
<Panel
id="chat-panel"
ref={chatPanelRef}
collapsible
minSize={1}
className={cn(!isResizing && "transition-all duration-100 ease-in-out")}
>
<div className="h-full w-full">
<ChatPanel
chatId={chatId}
isPreviewOpen={isPreviewOpen}
onTogglePreview={() => {
setIsPreviewOpen(!isPreviewOpen);
if (isPreviewOpen) {
ref.current?.collapse();
} else {
ref.current?.expand();
}
}}
/>
{!isChatPanelHidden && (
<ChatPanel
chatId={chatId}
isPreviewOpen={isPreviewOpen}
onTogglePreview={() => {
setIsPreviewOpen(!isPreviewOpen);
if (isPreviewOpen) {
ref.current?.collapse();
} else {
ref.current?.expand();
}
}}
/>
)}
</div>
</Panel>
<PanelResizeHandle
onDragging={(isDragging) => {
setIsResizing(isDragging);
// When dragging ends, sync the hidden state based on final width
if (!isDragging) {
// Small delay to let the panel settle
requestAnimationFrame(() => {
const panel = document.getElementById("chat-panel");
if (panel) {
const panelWidth = panel.getBoundingClientRect().width;
const containerWidth =
panel.parentElement?.getBoundingClientRect().width || 1;
const percentage = (panelWidth / containerWidth) * 100;
// Consider hidden if panel is less than 5% width
setIsChatPanelHidden(percentage < 5);
}
});
}
}}
className={cn(
"relative bg-gray-200 hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors cursor-col-resize",
isChatPanelHidden ? "w-2" : "w-1",
)}
/>
<>
<PanelResizeHandle
onDragging={(e) => setIsResizing(e)}
className="w-1 bg-gray-200 hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700 transition-colors cursor-col-resize"
/>
<Panel
collapsible
ref={ref}
id="preview-panel"
minSize={20}
className={cn(
!isResizing && "transition-all duration-100 ease-in-out",
)}
>
<PreviewPanel />
</Panel>
</>
<Panel
collapsible
ref={ref}
id="preview-panel"
minSize={20}
className={cn(!isResizing && "transition-all duration-100 ease-in-out")}
>
<PreviewPanel />
</Panel>
</PanelGroup>
);
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论