deerflow2/frontend/src/app/workspace/layout.tsx

155 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useSearchParams } from "next/navigation";
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { Toaster } from "@/components/ui/sonner";
import { CommandPalette } from "@/components/workspace/command-palette";
import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar";
import { getLocalSettings, useLocalSettings } from "@/core/settings";
const queryClient = new QueryClient();
export default function WorkspaceLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
const [settings, setSettings] = useLocalSettings();
const [open, setOpen] = useState(false); // SSR default: open (matches server render)
const [showWorkspaceSidebar, setShowWorkspaceSidebar] = useState(false);
const pressedKeysRef = useRef<Set<string>>(new Set());
const comboTriggeredRef = useRef(false);
const searchParams = useSearchParams();
// iframe 技能模式mode=skill时隐藏侧边栏
const isSkillMode = searchParams.get("mode") === "skill";
useLayoutEffect(() => {
// Runs synchronously before first paint on the client — no visual flash
setOpen(!getLocalSettings().layout.sidebar_collapsed);
}, []);
useEffect(() => {
setOpen(!settings.layout.sidebar_collapsed);
}, [settings.layout.sidebar_collapsed]);
useEffect(() => {
const resetComboTrigger = () => {
comboTriggeredRef.current = false;
};
const handleKeyDown = (event: KeyboardEvent) => {
const target = event.target as HTMLElement | null;
if (
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target?.isContentEditable
) {
return;
}
pressedKeysRef.current.add(event.key.toLowerCase());
const hasCtrlOrMeta = event.ctrlKey || event.metaKey;
const hasShift = event.shiftKey;
const hasL = pressedKeysRef.current.has("l");
const hasD = pressedKeysRef.current.has("d");
if (
hasCtrlOrMeta &&
hasShift &&
hasL &&
hasD &&
!comboTriggeredRef.current
) {
event.preventDefault();
comboTriggeredRef.current = true;
setShowWorkspaceSidebar((prev) => !prev);
}
};
const handleKeyUp = (event: KeyboardEvent) => {
pressedKeysRef.current.delete(event.key.toLowerCase());
if (
!pressedKeysRef.current.has("l") ||
!pressedKeysRef.current.has("d") ||
(!event.ctrlKey && !event.metaKey) ||
!event.shiftKey
) {
resetComboTrigger();
}
};
const handleBlur = () => {
pressedKeysRef.current.clear();
resetComboTrigger();
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
window.addEventListener("blur", handleBlur);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
window.removeEventListener("blur", handleBlur);
};
}, []);
const handleOpenChange = useCallback(
(open: boolean) => {
setOpen(open);
setSettings("layout", { sidebar_collapsed: !open });
},
[setSettings],
);
return (
<QueryClientProvider client={queryClient}>
<SidebarProvider
className="h-screen"
open={open}
onOpenChange={handleOpenChange}
>
{!isSkillMode && showWorkspaceSidebar && (
<WorkspaceSidebar className="" />
)}
<SidebarInset className="min-w-0">{children}</SidebarInset>
</SidebarProvider>
<CommandPalette />
<Toaster
position="top-center"
toastOptions={{
duration: 2200,
classNames: {
toast: [
/* 灰色圆角矩形容器 */
"rounded-[20px] border-none",
/* 浅灰色背景 + 轻微透明 */
"bg-ws-999999! backdrop-blur-sm",
/* 阴影极轻 */
"shadow-[0_2px_12px_0_rgba(0,0,0,0.18)]",
/* 内边距:宽松居中 */
"px-5 py-2.5",
/* 单行布局,内容水平居中 */
"flex items-center justify-center gap-0",
/* 整体文字样式 */
"text-primary-foreground text-sm font-normal font-sans",
/* 去掉 icon 区域间距 */
"[&>[data-icon]]:hidden",
].join(" "),
title:
"text-primary-foreground! text-sm font-normal text-center w-full leading-snug",
description: "hidden",
icon: "hidden",
},
}}
/>
</QueryClientProvider>
);
}