diff --git a/frontend/src/app/workspace/layout.tsx b/frontend/src/app/workspace/layout.tsx index 98b869b4..68b660dd 100644 --- a/frontend/src/app/workspace/layout.tsx +++ b/frontend/src/app/workspace/layout.tsx @@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useLayoutEffect, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { Toaster } from "@/components/ui/sonner"; @@ -16,6 +16,9 @@ export default function WorkspaceLayout({ }: 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>(new Set()); + const comboTriggeredRef = useRef(false); const searchParams = useSearchParams(); // iframe 技能模式(mode=skill)时隐藏侧边栏 @@ -28,6 +31,69 @@ export default function WorkspaceLayout({ 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); @@ -42,8 +108,7 @@ export default function WorkspaceLayout({ open={open} onOpenChange={handleOpenChange} > - {/* MARK:!!!! 生产环境下必须注释才能提交!!!! */} - {/* {!isSkillMode && } */} + {!isSkillMode && showWorkspaceSidebar && } {children} (
-
+
); diff --git a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx index 64e5cbe2..25047e50 100644 --- a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -52,6 +52,7 @@ export function ArtifactFileDetail({ const { t } = useI18n(); const { artifacts, setOpen, select, fullscreen, setFullscreen } = useArtifacts(); + const isWriteFile = useMemo(() => { return filepathFromProps.startsWith("write-file:"); }, [filepathFromProps]); @@ -62,6 +63,8 @@ export function ArtifactFileDetail({ } return filepathFromProps; }, [filepathFromProps, isWriteFile]); + // 获取文件名(不含路径) + const fileName = useMemo(() => getFileName(filepath), [filepath]); const isSkillFile = useMemo(() => { return filepath.endsWith(".skill"); }, [filepath]); @@ -80,6 +83,19 @@ export function ArtifactFileDetail({ const previewable = useMemo(() => { return (language === "html" && !isWriteFile) || language === "markdown"; }, [isWriteFile, language]); + const artifactUrl = useMemo(() => { + return urlOfArtifact({ filepath, threadId }); + }, [filepath, threadId]); + const artifactPreviewKind = useMemo(() => { + return getArtifactPreviewKind(filepath); + }, [filepath]); + const artifactViewerSrcDoc = useMemo(() => { + return buildArtifactViewerSrcDoc({ + artifactUrl, + fileName, + kind: artifactPreviewKind, + }); + }, [artifactUrl, fileName, artifactPreviewKind]); const { content } = useArtifactContent({ threadId, filepath: filepathFromProps, @@ -99,8 +115,6 @@ export function ArtifactFileDetail({ const [isInstalling, setIsInstalling] = useState(false); const [zoom, setZoom] = useState(80); - // 获取文件名(不含路径) - const fileName = useMemo(() => getFileName(filepath), [filepath]); // 是否可以转换为docx/pdf(仅markdown文件支持) const canConvertToDocxPdf = language === "markdown"; @@ -444,7 +458,7 @@ export function ArtifactFileDetail({ {/* 遮挡多余的滚动顶部 */} -
+ {/*
*/} {previewable && viewMode === "preview" && (language === "markdown" || language === "html") && ( @@ -464,8 +478,10 @@ export function ArtifactFileDetail({ )} {!isCodeFile && ( `; + } + if (kind === "html") { + return ``; + } + return `
+

${safeName}

+

This file type is not previewable in the custom viewer.

+ Open in new tab +
`; + })(); + + return ` + + + + + + + + ${content} + +`; +} + // 缩放比例选项 const ZOOM_LEVELS = [50, 60, 70, 80, 90, 100, 110, 120, 130, 150, 175, 200]; diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts index 90509445..bfbcbbe3 100644 --- a/frontend/src/core/i18n/locales/zh-CN.ts +++ b/frontend/src/core/i18n/locales/zh-CN.ts @@ -53,7 +53,7 @@ export const zhCN: Translations = { // Welcome welcome: { - greeting: "轻办公.XClaw", + greeting: "轻办公 · XClaw", description: "欢迎使用 🦌 DeerFlow,一个完全开源的超级智能体。通过内置和自定义的 Skills,\nDeerFlow 可以帮你搜索网络、分析数据,还能为你生成幻灯片、\n图片、视频、播客及网页等,几乎可以做任何事情。",