feat: enhance public task markdown preview
This commit is contained in:
1 parent
6157b42f5a
commit
0dd93421c4
1 file changed
+161
-3
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
BookOpen,
|
||||
Check,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
@@ -46,7 +47,9 @@ import { getBackendBaseURL } from "@/core/config";
|
||||
import { usePublicBackground } from "@/core/embed/background";
|
||||
import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
|
||||
import { reasoningPlugins } from "@/core/streamdown";
|
||||
import { createMarkdownAnchorResolver } from "@/core/utils/markdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
import "@/components/wiki/wiki.css";
|
||||
|
||||
type PublicTask = {
|
||||
task_id: string;
|
||||
@@ -264,6 +267,153 @@ function stripTimestampSuffix(title: string): string {
|
||||
return cleaned.trim() || title;
|
||||
}
|
||||
|
||||
function stripMarkdownToText(markdown: string): string {
|
||||
return markdown
|
||||
.replace(/```[\s\S]*?```/g, " ")
|
||||
.replace(/~~~[\s\S]*?~~~/g, " ")
|
||||
.replace(/!\[[^\]]*\]\([^)]+\)/g, " ")
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||
.replace(/^#{1,6}\s+/gm, "")
|
||||
.replace(/^[>\-*+\d.)\s]+/gm, "")
|
||||
.replace(/[`*_~|]/g, "")
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getMarkdownExcerpt(markdown: string): string {
|
||||
const text = stripMarkdownToText(markdown);
|
||||
if (!text) return "";
|
||||
return text.length > 180 ? `${text.slice(0, 180)}...` : text;
|
||||
}
|
||||
|
||||
function countReadableWords(markdown: string): number {
|
||||
const text = stripMarkdownToText(markdown);
|
||||
if (!text) return 0;
|
||||
const cjk = text.match(/[\u4e00-\u9fff]/g)?.length ?? 0;
|
||||
const words = text.replace(/[\u4e00-\u9fff]/g, " ").match(/[A-Za-z0-9]+/g)?.length ?? 0;
|
||||
return cjk + words;
|
||||
}
|
||||
|
||||
function selectorForId(id: string): string {
|
||||
const css = window.CSS;
|
||||
if (css?.escape) return `#${css.escape(id)}`;
|
||||
return `[id="${id.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"]`;
|
||||
}
|
||||
|
||||
function KnowledgeMarkdownView({
|
||||
content,
|
||||
title,
|
||||
subtitle,
|
||||
className,
|
||||
}: {
|
||||
content: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
className?: string;
|
||||
isLoading?: boolean;
|
||||
remarkPlugins?: unknown;
|
||||
rehypePlugins?: unknown;
|
||||
}) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const anchorResolver = useMemo(() => createMarkdownAnchorResolver(content), [content]);
|
||||
const headings = useMemo(
|
||||
() => anchorResolver.headings.filter((item) => item.level >= 1 && item.level <= 3),
|
||||
[anchorResolver],
|
||||
);
|
||||
const excerpt = useMemo(() => getMarkdownExcerpt(content), [content]);
|
||||
const sectionCount = headings.length;
|
||||
const wordCount = useMemo(() => countReadableWords(content), [content]);
|
||||
|
||||
const scrollToHeading = useCallback((id: string) => {
|
||||
const root = rootRef.current;
|
||||
const target =
|
||||
root?.querySelector<HTMLElement>(selectorForId(id)) ?? document.getElementById(id);
|
||||
target?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={cn(
|
||||
"grid min-h-0 gap-4 lg:grid-cols-[minmax(0,1fr)_260px]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<article className="min-w-0 rounded-md border border-slate-200/70 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-950">
|
||||
<div className="border-b border-slate-100 bg-slate-50/80 px-5 py-4 dark:border-slate-800 dark:bg-slate-900/60">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-white px-2 py-1 font-medium text-slate-600 ring-1 ring-slate-200 dark:bg-slate-950 dark:text-slate-300 dark:ring-slate-800">
|
||||
<BookOpen className="size-3.5" />
|
||||
知识梳理
|
||||
</span>
|
||||
{subtitle && <span className="truncate">{subtitle}</span>}
|
||||
</div>
|
||||
{title && (
|
||||
<h1 className="mt-3 text-xl font-semibold leading-snug text-slate-950 dark:text-slate-50 md:text-2xl">
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
{excerpt && (
|
||||
<p className="mt-3 max-w-4xl text-sm leading-7 text-slate-600 dark:text-slate-300">
|
||||
{excerpt}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-4 flex flex-wrap gap-2 text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="rounded-md bg-white px-2 py-1 ring-1 ring-slate-200 dark:bg-slate-950 dark:ring-slate-800">
|
||||
{sectionCount || 1} 个章节
|
||||
</span>
|
||||
<span className="rounded-md bg-white px-2 py-1 ring-1 ring-slate-200 dark:bg-slate-950 dark:ring-slate-800">
|
||||
约 {wordCount} 字
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="wiki-markdown px-5 py-5 md:px-7 md:py-6">
|
||||
<MarkdownContent
|
||||
className="max-w-none"
|
||||
content={content}
|
||||
isLoading={false}
|
||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside className="hidden min-w-0 lg:block">
|
||||
<div className="sticky top-3 rounded-md border border-slate-200/70 bg-white p-3 shadow-sm dark:border-slate-800 dark:bg-slate-950">
|
||||
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-slate-800 dark:text-slate-100">
|
||||
<List className="size-4" />
|
||||
内容目录
|
||||
</div>
|
||||
{headings.length > 0 ? (
|
||||
<nav className="max-h-[calc(100vh-260px)] space-y-1 overflow-auto pr-1">
|
||||
{headings.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
"block w-full truncate rounded-md px-2 py-1.5 text-left text-xs text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-950 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-slate-50",
|
||||
item.level === 2 && "pl-4",
|
||||
item.level === 3 && "pl-7 text-slate-500 dark:text-slate-400",
|
||||
)}
|
||||
title={item.text}
|
||||
onClick={() => scrollToHeading(item.id)}
|
||||
>
|
||||
{item.text}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
) : (
|
||||
<div className="rounded-md border border-dashed p-3 text-xs text-slate-500 dark:border-slate-800 dark:text-slate-400">
|
||||
暂无标题目录
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BODY_TAB_VALUE = "__body__";
|
||||
const HTML_TAB_VALUE = "__html__";
|
||||
const RUN_LIST_COLLAPSED_WIDTH = 56;
|
||||
@@ -688,9 +838,10 @@ function PublicFullscreenPreviewContent({ preview }: { preview: PublicFullscreen
|
||||
}
|
||||
if (preview.mode === "markdown") {
|
||||
return (
|
||||
<MarkdownContent
|
||||
<KnowledgeMarkdownView
|
||||
className="max-w-none"
|
||||
content={preview.content || "无正文内容"}
|
||||
title={preview.title}
|
||||
isLoading={false}
|
||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||
@@ -701,9 +852,10 @@ function PublicFullscreenPreviewContent({ preview }: { preview: PublicFullscreen
|
||||
const content = preview.file.content || "无文件内容";
|
||||
if (isMarkdownFile(preview.file)) {
|
||||
return (
|
||||
<MarkdownContent
|
||||
<KnowledgeMarkdownView
|
||||
className="max-w-none"
|
||||
content={content}
|
||||
title={preview.title}
|
||||
isLoading={false}
|
||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||
@@ -1122,9 +1274,15 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
|
||||
无内容。点击「编辑」开始添加。
|
||||
</div>
|
||||
) : (
|
||||
<MarkdownContent
|
||||
<KnowledgeMarkdownView
|
||||
className="max-w-none pr-2"
|
||||
content={activeSource}
|
||||
title={
|
||||
activeTabMeta?.kind === "file"
|
||||
? stripExtension(activeTabMeta.fileName || "")
|
||||
: run.result?.title
|
||||
}
|
||||
subtitle={`执行时间:${formatDate(run.scheduled_for)}`}
|
||||
isLoading={false}
|
||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||
|
||||
Reference in new issue
Block a user