feat: improve markdown rendering, artifact file preview, and public scheduled task page

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AdministratorandClaude Opus 4.8 committed 2026-06-04 16:52:30 +08:00
1 parent 94c02ded2a
commit 87f764bfa9
5 files changed
+419 -74

No files matched your search

@@ -1,7 +1,7 @@
"use client";
import {
ArrowDownIcon,
import {
ArrowDownIcon,
Code2Icon,
CopyIcon,
DownloadIcon,
@@ -12,10 +12,11 @@ import {
SaveIcon,
SquareArrowOutUpRightIcon,
XIcon,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { Streamdown } from "streamdown";
} from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from "react";
import { toast } from "sonner";
import { Streamdown } from "streamdown";
import {
Artifact,
@@ -47,9 +48,10 @@ import { urlOfArtifact } from "@/core/artifacts/utils";
import { useI18n } from "@/core/i18n/hooks";
import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
import { installSkill } from "@/core/skills/api";
import { streamdownPlugins } from "@/core/streamdown";
import { checkCodeFile, getFileName } from "@/core/utils/files";
import { env } from "@/env";
import { streamdownPlugins } from "@/core/streamdown";
import { checkCodeFile, getFileName } from "@/core/utils/files";
import { createMarkdownAnchorResolver } from "@/core/utils/markdown";
import { env } from "@/env";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
@@ -59,9 +61,15 @@ import { Tooltip } from "../tooltip";
import { useArtifacts } from "./context";
const SANDBOX_RESUME_EVENT = "workspace:sandbox-resume-autoscroll";
function findScrollableArtifactElement(root: HTMLElement | null) {
const SANDBOX_RESUME_EVENT = "workspace:sandbox-resume-autoscroll";
function extractTextFromChildren(children: ReactNode): string {
if (typeof children === "string" || typeof children === "number") return String(children);
if (Array.isArray(children)) return children.map(extractTextFromChildren).join("");
return "";
}
function findScrollableArtifactElement(root: HTMLElement | null) {
if (!root) {
return null;
}
@@ -706,10 +714,54 @@ export function ArtifactFilePreview({
language: string;
animated?: boolean;
activeCursor?: boolean;
}) {
const [htmlPreviewUrl, setHtmlPreviewUrl] = useState<string>();
useEffect(() => {
}) {
const [htmlPreviewUrl, setHtmlPreviewUrl] = useState<string>();
const markdownComponents = useMemo(() => {
const anchorResolver = createMarkdownAnchorResolver(content ?? "");
const heading = (level: 1 | 2 | 3 | 4 | 5 | 6) => {
const Tag = `h${level}` as const;
return ({ children, id, ...props }: HTMLAttributes<HTMLHeadingElement>) => (
<Tag id={id ?? anchorResolver.getHeadingId(level, extractTextFromChildren(children))} {...props}>
{children}
</Tag>
);
};
return {
h1: heading(1),
h2: heading(2),
h3: heading(3),
h4: heading(4),
h5: heading(5),
h6: heading(6),
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {
if (typeof props.children === "string" && /^citation:(.+)$/.test(props.children)) {
return <ArtifactLink {...props} />;
}
const { className, ...rest } = props;
const resolvedHref = anchorResolver.resolveHref(
props.href,
extractTextFromChildren(props.children),
);
if (resolvedHref === null) {
return (
<span
className={cn(
"text-primary/80 decoration-dotted underline underline-offset-2 cursor-not-allowed",
className,
)}
title="未在当前 Markdown 中找到对应章节"
>
{props.children}
</span>
);
}
return <ArtifactLink {...rest} className={className} href={resolvedHref ?? props.href} />;
},
};
}, [content]);
useEffect(() => {
if (language !== "html") {
setHtmlPreviewUrl(undefined);
return;
@@ -733,11 +785,11 @@ export function ArtifactFilePreview({
animated && "animate-[sandbox-preview-enter_320ms_ease-out]",
)}
>
<Streamdown
className="size-full"
{...streamdownPlugins}
components={{ a: ArtifactLink }}
>
<Streamdown
className="size-full"
{...streamdownPlugins}
components={markdownComponents}
>
{content ?? ""}
</Streamdown>
{activeCursor && (
@@ -1,20 +1,27 @@
"use client";
import { useMemo } from "react";
import type { AnchorHTMLAttributes } from "react";
import { useMemo } from "react";
import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from "react";
import {
MessageResponse,
type MessageResponseProps,
} from "@/components/ai-elements/message";
import { streamdownPlugins } from "@/core/streamdown";
import { cn } from "@/lib/utils";
import { streamdownPlugins } from "@/core/streamdown";
import { createMarkdownAnchorResolver } from "@/core/utils/markdown";
import { cn } from "@/lib/utils";
import { CitationLink } from "../citations/citation-link";
function isExternalUrl(href: string | undefined): boolean {
return !!href && /^https?:\/\//.test(href);
}
function isExternalUrl(href: string | undefined): boolean {
return !!href && /^https?:\/\//.test(href);
}
function extractTextFromChildren(children: ReactNode): string {
if (typeof children === "string" || typeof children === "number") return String(children);
if (Array.isArray(children)) return children.map(extractTextFromChildren).join("");
return "";
}
export type MarkdownContentProps = {
content: string;
@@ -32,12 +39,28 @@ export function MarkdownContent({
className,
remarkPlugins = streamdownPlugins.remarkPlugins,
components: componentsFromProps,
}: MarkdownContentProps) {
const components = useMemo(() => {
return {
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {
if (props.href?.startsWith("#reference-")) {
return <CitationLink {...props} />;
}: MarkdownContentProps) {
const anchorResolver = useMemo(() => createMarkdownAnchorResolver(content), [content]);
const components = useMemo(() => {
const heading = (level: 1 | 2 | 3 | 4 | 5 | 6) => {
const Tag = `h${level}` as const;
return ({ children, id, ...props }: HTMLAttributes<HTMLHeadingElement>) => (
<Tag id={id ?? anchorResolver.getHeadingId(level, extractTextFromChildren(children))} {...props}>
{children}
</Tag>
);
};
return {
h1: heading(1),
h2: heading(2),
h3: heading(3),
h4: heading(4),
h5: heading(5),
h6: heading(6),
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {
if (props.href?.startsWith("#reference-")) {
return <CitationLink {...props} />;
}
if (typeof props.children === "string") {
const match = /^citation:(.+)$/.exec(props.children);
@@ -46,23 +69,41 @@ export function MarkdownContent({
return <CitationLink {...props}>{text}</CitationLink>;
}
}
const { className, target, rel, ...rest } = props;
const external = isExternalUrl(props.href);
return (
<a
{...rest}
className={cn(
"text-primary decoration-primary/30 hover:decoration-primary/60 underline underline-offset-2 transition-colors",
className,
const { className, target, rel, ...rest } = props;
const external = isExternalUrl(props.href);
const resolvedHref = anchorResolver.resolveHref(
props.href,
extractTextFromChildren(props.children),
);
if (resolvedHref === null) {
return (
<span
className={cn(
"text-primary/80 decoration-dotted underline underline-offset-2 cursor-not-allowed",
className,
)}
title="未在当前 Markdown 中找到对应章节"
>
{props.children}
</span>
);
}
return (
<a
{...rest}
href={resolvedHref ?? props.href}
className={cn(
"text-primary decoration-primary/30 hover:decoration-primary/60 underline underline-offset-2 transition-colors",
className,
)}
target={target ?? (external ? "_blank" : undefined)}
rel={rel ?? (external ? "noopener noreferrer" : undefined)}
/>
);
},
...componentsFromProps,
};
}, [componentsFromProps]);
},
...componentsFromProps,
};
}, [anchorResolver, componentsFromProps]);
if (!content) return null;