feat: 更改系统名的. 为 · ;调整结果布局大小;为侧边栏添加按键显示逻辑ctl+shift+l+d
This commit is contained in:
parent
7b3142c580
commit
b6b373ffe2
|
|
@ -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<Set<string>>(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 && <WorkspaceSidebar className="" />} */}
|
||||
{!isSkillMode && showWorkspaceSidebar && <WorkspaceSidebar className="" />}
|
||||
<SidebarInset className="min-w-0">{children}</SidebarInset>
|
||||
</SidebarProvider>
|
||||
<Toaster
|
||||
|
|
|
|||
|
|
@ -144,6 +144,6 @@ export const ArtifactContent = ({
|
|||
...props
|
||||
}: ArtifactContentProps) => (
|
||||
<div className="min-h-0 rounded-[10px] flex-1 overflow-auto">
|
||||
<div className={cn("mb-[150px] p-4", className)} {...props} />
|
||||
<div className={cn("mb-[150px] h-full p-4", className)} {...props} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
</ArtifactHeader>
|
||||
<ArtifactContent className=" rounded-b-[10px] bg-white p-0">
|
||||
{/* 遮挡多余的滚动顶部 */}
|
||||
<div className="absolute w-[calc(100%-40px)] bg-white z-20 h-5 rounded-t-[10px] top-[57px]"></div>
|
||||
{/* <div className="absolute w-[calc(100%-40px)] bg-white z-20 h-5 rounded-t-[10px] top-[57px]"></div> */}
|
||||
{previewable &&
|
||||
viewMode === "preview" &&
|
||||
(language === "markdown" || language === "html") && (
|
||||
|
|
@ -464,8 +478,10 @@ export function ArtifactFileDetail({
|
|||
)}
|
||||
{!isCodeFile && (
|
||||
<iframe
|
||||
className="size-full"
|
||||
src={urlOfArtifact({ filepath, threadId })}
|
||||
className="size-full border-0"
|
||||
srcDoc={artifactViewerSrcDoc}
|
||||
sandbox="allow-same-origin allow-scripts allow-downloads"
|
||||
title={`Artifact preview: ${fileName}`}
|
||||
/>
|
||||
)}
|
||||
</ArtifactContent>
|
||||
|
|
@ -514,6 +530,166 @@ export function ArtifactFilePreview({
|
|||
return null;
|
||||
}
|
||||
|
||||
type ArtifactPreviewKind = "html" | "image" | "video" | "audio" | "pdf" | "other";
|
||||
|
||||
function getArtifactPreviewKind(filepath: string): ArtifactPreviewKind {
|
||||
const lower = filepath.toLowerCase();
|
||||
if (/\.(html?|xhtml)$/.test(lower)) return "html";
|
||||
if (/\.(png|jpe?g|gif|webp|bmp|svg|ico|avif|tiff?)$/.test(lower)) return "image";
|
||||
if (/\.(mp4|webm|ogg|mov|m4v)$/.test(lower)) return "video";
|
||||
if (/\.(mp3|wav|ogg|m4a|aac|flac)$/.test(lower)) return "audio";
|
||||
if (/\.pdf$/.test(lower)) return "pdf";
|
||||
return "other";
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function buildArtifactViewerSrcDoc({
|
||||
artifactUrl,
|
||||
fileName,
|
||||
kind,
|
||||
}: {
|
||||
artifactUrl: string;
|
||||
fileName: string;
|
||||
kind: ArtifactPreviewKind;
|
||||
}) {
|
||||
const safeUrl = escapeHtml(artifactUrl);
|
||||
const safeName = escapeHtml(fileName);
|
||||
|
||||
const content = (() => {
|
||||
if (kind === "image") {
|
||||
return `<img class="preview image" src="${safeUrl}" alt="${safeName}" />`;
|
||||
}
|
||||
if (kind === "video") {
|
||||
return `<video class="preview media" src="${safeUrl}" controls playsinline preload="metadata"></video>`;
|
||||
}
|
||||
if (kind === "audio") {
|
||||
return `<div class="audio-wrap"><audio class="audio" src="${safeUrl}" controls preload="metadata"></audio></div>`;
|
||||
}
|
||||
if (kind === "pdf") {
|
||||
return `<iframe class="preview frame" src="${safeUrl}#view=FitH"></iframe>`;
|
||||
}
|
||||
if (kind === "html") {
|
||||
return `<iframe class="preview frame" src="${safeUrl}" sandbox="allow-scripts allow-forms allow-modals allow-popups allow-downloads"></iframe>`;
|
||||
}
|
||||
return `<div class="fallback">
|
||||
<p class="title">${safeName}</p>
|
||||
<p class="desc">This file type is not previewable in the custom viewer.</p>
|
||||
<a class="link" href="${safeUrl}" target="_blank" rel="noopener noreferrer">Open in new tab</a>
|
||||
</div>`;
|
||||
})();
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f8f9fb;
|
||||
--panel: #ffffff;
|
||||
--text: #0f172a;
|
||||
--muted: #667085;
|
||||
--line: #e4e7ec;
|
||||
--radius: 12px;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
overflow: hidden;
|
||||
padding: 12px;
|
||||
}
|
||||
.preview {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel);
|
||||
display: block;
|
||||
}
|
||||
.image {
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
background:
|
||||
linear-gradient(45deg, #f4f4f5 25%, transparent 25%, transparent 75%, #f4f4f5 75%, #f4f4f5) 0 0/16px 16px,
|
||||
linear-gradient(45deg, #f4f4f5 25%, transparent 25%, transparent 75%, #f4f4f5 75%, #f4f4f5) 8px 8px/16px 16px,
|
||||
#fff;
|
||||
}
|
||||
.media {
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
.frame {
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
.audio-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
.audio {
|
||||
width: min(720px, 100%);
|
||||
}
|
||||
.fallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel);
|
||||
display: grid;
|
||||
place-content: center;
|
||||
text-align: center;
|
||||
gap: 10px;
|
||||
padding: 24px;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
}
|
||||
.desc {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.link {
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.link:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${content}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
// 缩放比例选项
|
||||
const ZOOM_LEVELS = [50, 60, 70, 80, 90, 100, 110, 120, 130, 150, 175, 200];
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export const zhCN: Translations = {
|
|||
|
||||
// Welcome
|
||||
welcome: {
|
||||
greeting: "轻办公.XClaw",
|
||||
greeting: "轻办公 · XClaw",
|
||||
description:
|
||||
"欢迎使用 🦌 DeerFlow,一个完全开源的超级智能体。通过内置和自定义的 Skills,\nDeerFlow 可以帮你搜索网络、分析数据,还能为你生成幻灯片、\n图片、视频、播客及网页等,几乎可以做任何事情。",
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue