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 { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { useSearchParams } from "next/navigation";
|
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 { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
|
@ -16,6 +16,9 @@ export default function WorkspaceLayout({
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
const [settings, setSettings] = useLocalSettings();
|
const [settings, setSettings] = useLocalSettings();
|
||||||
const [open, setOpen] = useState(false); // SSR default: open (matches server render)
|
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();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
// iframe 技能模式(mode=skill)时隐藏侧边栏
|
// iframe 技能模式(mode=skill)时隐藏侧边栏
|
||||||
|
|
@ -28,6 +31,69 @@ export default function WorkspaceLayout({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpen(!settings.layout.sidebar_collapsed);
|
setOpen(!settings.layout.sidebar_collapsed);
|
||||||
}, [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(
|
const handleOpenChange = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
setOpen(open);
|
setOpen(open);
|
||||||
|
|
@ -42,8 +108,7 @@ export default function WorkspaceLayout({
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
>
|
>
|
||||||
{/* MARK:!!!! 生产环境下必须注释才能提交!!!! */}
|
{!isSkillMode && showWorkspaceSidebar && <WorkspaceSidebar className="" />}
|
||||||
{/* {!isSkillMode && <WorkspaceSidebar className="" />} */}
|
|
||||||
<SidebarInset className="min-w-0">{children}</SidebarInset>
|
<SidebarInset className="min-w-0">{children}</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
<Toaster
|
<Toaster
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,6 @@ export const ArtifactContent = ({
|
||||||
...props
|
...props
|
||||||
}: ArtifactContentProps) => (
|
}: ArtifactContentProps) => (
|
||||||
<div className="min-h-0 rounded-[10px] flex-1 overflow-auto">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ export function ArtifactFileDetail({
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { artifacts, setOpen, select, fullscreen, setFullscreen } =
|
const { artifacts, setOpen, select, fullscreen, setFullscreen } =
|
||||||
useArtifacts();
|
useArtifacts();
|
||||||
|
|
||||||
const isWriteFile = useMemo(() => {
|
const isWriteFile = useMemo(() => {
|
||||||
return filepathFromProps.startsWith("write-file:");
|
return filepathFromProps.startsWith("write-file:");
|
||||||
}, [filepathFromProps]);
|
}, [filepathFromProps]);
|
||||||
|
|
@ -62,6 +63,8 @@ export function ArtifactFileDetail({
|
||||||
}
|
}
|
||||||
return filepathFromProps;
|
return filepathFromProps;
|
||||||
}, [filepathFromProps, isWriteFile]);
|
}, [filepathFromProps, isWriteFile]);
|
||||||
|
// 获取文件名(不含路径)
|
||||||
|
const fileName = useMemo(() => getFileName(filepath), [filepath]);
|
||||||
const isSkillFile = useMemo(() => {
|
const isSkillFile = useMemo(() => {
|
||||||
return filepath.endsWith(".skill");
|
return filepath.endsWith(".skill");
|
||||||
}, [filepath]);
|
}, [filepath]);
|
||||||
|
|
@ -80,6 +83,19 @@ export function ArtifactFileDetail({
|
||||||
const previewable = useMemo(() => {
|
const previewable = useMemo(() => {
|
||||||
return (language === "html" && !isWriteFile) || language === "markdown";
|
return (language === "html" && !isWriteFile) || language === "markdown";
|
||||||
}, [isWriteFile, language]);
|
}, [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({
|
const { content } = useArtifactContent({
|
||||||
threadId,
|
threadId,
|
||||||
filepath: filepathFromProps,
|
filepath: filepathFromProps,
|
||||||
|
|
@ -99,8 +115,6 @@ export function ArtifactFileDetail({
|
||||||
const [isInstalling, setIsInstalling] = useState(false);
|
const [isInstalling, setIsInstalling] = useState(false);
|
||||||
const [zoom, setZoom] = useState(80);
|
const [zoom, setZoom] = useState(80);
|
||||||
|
|
||||||
// 获取文件名(不含路径)
|
|
||||||
const fileName = useMemo(() => getFileName(filepath), [filepath]);
|
|
||||||
|
|
||||||
// 是否可以转换为docx/pdf(仅markdown文件支持)
|
// 是否可以转换为docx/pdf(仅markdown文件支持)
|
||||||
const canConvertToDocxPdf = language === "markdown";
|
const canConvertToDocxPdf = language === "markdown";
|
||||||
|
|
@ -444,7 +458,7 @@ export function ArtifactFileDetail({
|
||||||
</ArtifactHeader>
|
</ArtifactHeader>
|
||||||
<ArtifactContent className=" rounded-b-[10px] bg-white p-0">
|
<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 &&
|
{previewable &&
|
||||||
viewMode === "preview" &&
|
viewMode === "preview" &&
|
||||||
(language === "markdown" || language === "html") && (
|
(language === "markdown" || language === "html") && (
|
||||||
|
|
@ -464,8 +478,10 @@ export function ArtifactFileDetail({
|
||||||
)}
|
)}
|
||||||
{!isCodeFile && (
|
{!isCodeFile && (
|
||||||
<iframe
|
<iframe
|
||||||
className="size-full"
|
className="size-full border-0"
|
||||||
src={urlOfArtifact({ filepath, threadId })}
|
srcDoc={artifactViewerSrcDoc}
|
||||||
|
sandbox="allow-same-origin allow-scripts allow-downloads"
|
||||||
|
title={`Artifact preview: ${fileName}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ArtifactContent>
|
</ArtifactContent>
|
||||||
|
|
@ -514,6 +530,166 @@ export function ArtifactFilePreview({
|
||||||
return null;
|
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];
|
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
|
||||||
welcome: {
|
welcome: {
|
||||||
greeting: "轻办公.XClaw",
|
greeting: "轻办公 · XClaw",
|
||||||
description:
|
description:
|
||||||
"欢迎使用 🦌 DeerFlow,一个完全开源的超级智能体。通过内置和自定义的 Skills,\nDeerFlow 可以帮你搜索网络、分析数据,还能为你生成幻灯片、\n图片、视频、播客及网页等,几乎可以做任何事情。",
|
"欢迎使用 🦌 DeerFlow,一个完全开源的超级智能体。通过内置和自定义的 Skills,\nDeerFlow 可以帮你搜索网络、分析数据,还能为你生成幻灯片、\n图片、视频、播客及网页等,几乎可以做任何事情。",
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue