From b6b373ffe2f8cab543783934730d6b89bc73fa2e Mon Sep 17 00:00:00 2001 From: MT-Mint <798521692@qq.com> Date: Mon, 30 Mar 2026 12:56:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=94=B9=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E5=90=8D=E7=9A=84.=20=E4=B8=BA=20=C2=B7=20=EF=BC=9B=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E7=BB=93=E6=9E=9C=E5=B8=83=E5=B1=80=E5=A4=A7=E5=B0=8F?= =?UTF-8?q?=EF=BC=9B=E4=B8=BA=E4=BE=A7=E8=BE=B9=E6=A0=8F=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=8C=89=E9=94=AE=E6=98=BE=E7=A4=BA=E9=80=BB=E8=BE=91ctl+shift?= =?UTF-8?q?+l+d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/workspace/layout.tsx | 71 ++++++- .../src/components/ai-elements/artifact.tsx | 2 +- .../artifacts/artifact-file-detail.tsx | 186 +++++++++++++++++- frontend/src/core/i18n/locales/zh-CN.ts | 2 +- 4 files changed, 251 insertions(+), 10 deletions(-) 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图片、视频、播客及网页等,几乎可以做任何事情。",