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 { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
|
BookOpen,
|
||||||
Check,
|
Check,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@@ -46,7 +47,9 @@ import { getBackendBaseURL } from "@/core/config";
|
|||||||
import { usePublicBackground } from "@/core/embed/background";
|
import { usePublicBackground } from "@/core/embed/background";
|
||||||
import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
|
import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
|
||||||
import { reasoningPlugins } from "@/core/streamdown";
|
import { reasoningPlugins } from "@/core/streamdown";
|
||||||
|
import { createMarkdownAnchorResolver } from "@/core/utils/markdown";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import "@/components/wiki/wiki.css";
|
||||||
|
|
||||||
type PublicTask = {
|
type PublicTask = {
|
||||||
task_id: string;
|
task_id: string;
|
||||||
@@ -264,6 +267,153 @@ function stripTimestampSuffix(title: string): string {
|
|||||||
return cleaned.trim() || title;
|
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 BODY_TAB_VALUE = "__body__";
|
||||||
const HTML_TAB_VALUE = "__html__";
|
const HTML_TAB_VALUE = "__html__";
|
||||||
const RUN_LIST_COLLAPSED_WIDTH = 56;
|
const RUN_LIST_COLLAPSED_WIDTH = 56;
|
||||||
@@ -688,9 +838,10 @@ function PublicFullscreenPreviewContent({ preview }: { preview: PublicFullscreen
|
|||||||
}
|
}
|
||||||
if (preview.mode === "markdown") {
|
if (preview.mode === "markdown") {
|
||||||
return (
|
return (
|
||||||
<MarkdownContent
|
<KnowledgeMarkdownView
|
||||||
className="max-w-none"
|
className="max-w-none"
|
||||||
content={preview.content || "无正文内容"}
|
content={preview.content || "无正文内容"}
|
||||||
|
title={preview.title}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||||
@@ -701,9 +852,10 @@ function PublicFullscreenPreviewContent({ preview }: { preview: PublicFullscreen
|
|||||||
const content = preview.file.content || "无文件内容";
|
const content = preview.file.content || "无文件内容";
|
||||||
if (isMarkdownFile(preview.file)) {
|
if (isMarkdownFile(preview.file)) {
|
||||||
return (
|
return (
|
||||||
<MarkdownContent
|
<KnowledgeMarkdownView
|
||||||
className="max-w-none"
|
className="max-w-none"
|
||||||
content={content}
|
content={content}
|
||||||
|
title={preview.title}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||||
@@ -1122,9 +1274,15 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
|
|||||||
无内容。点击「编辑」开始添加。
|
无内容。点击「编辑」开始添加。
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<MarkdownContent
|
<KnowledgeMarkdownView
|
||||||
className="max-w-none pr-2"
|
className="max-w-none pr-2"
|
||||||
content={activeSource}
|
content={activeSource}
|
||||||
|
title={
|
||||||
|
activeTabMeta?.kind === "file"
|
||||||
|
? stripExtension(activeTabMeta.fileName || "")
|
||||||
|
: run.result?.title
|
||||||
|
}
|
||||||
|
subtitle={`执行时间:${formatDate(run.scheduled_for)}`}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||||
|
|||||||
Reference in new issue
Block a user