fix: strip YAML frontmatter and defer API reference markdown rendering (#17272)

This commit is contained in:
euvre
2026-07-24 11:37:10 +08:00
committed by GitHub
parent 86530931fe
commit 91d9bf7fc4
2 changed files with 95 additions and 23 deletions

View File

@@ -3,11 +3,33 @@ import { Button } from '@/components/ui/button';
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import { LangfuseCard } from '@/pages/user-setting/setting-model/langfuse';
import apiDoc from '@parent/docs/references/http_api_reference.md?raw';
import MarkdownPreview from '@uiw/react-markdown-preview';
import { Loader2 } from 'lucide-react';
import {
lazy,
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import ChatApiKeyModal from '../chat-api-key-modal';
import BackendServiceApi from './backend-service-api';
import MarkdownToc from './markdown-toc';
const LazyMarkdownPreview = lazy(() => import('@uiw/react-markdown-preview'));
const removeFrontmatter = (content: string): string => {
const lines = content.split('\n');
if (lines[0]?.trim() === '---') {
const endIndex = lines.slice(1).findIndex((line) => line.trim() === '---');
if (endIndex !== -1) {
return lines.slice(endIndex + 2).join('\n');
}
}
return content;
};
const ApiContent = ({ id, idKey }: { id?: string; idKey: string }) => {
const { t } = useTranslate('setting');
@@ -25,6 +47,25 @@ const ApiContent = ({ id, idKey }: { id?: string; idKey: string }) => {
const isDarkTheme = useIsDarkTheme();
const cleanDoc = useMemo(() => removeFrontmatter(apiDoc), []);
// Defer the heavy 230KB markdown rendering so the page paints immediately.
const [docReady, setDocReady] = useState(false);
useEffect(() => {
const id = requestAnimationFrame(() => setDocReady(true));
return () => cancelAnimationFrame(id);
}, []);
// Ref wrapping the lazily-mounted markdown preview. The TOC scopes its heading
// queries to this element, and only mounts once the preview is actually in the
// DOM (not while its lazy chunk is still suspended).
const previewRef = useRef<HTMLElement | null>(null);
const [previewMounted, setPreviewMounted] = useState(false);
const setPreviewRef = useCallback((el: HTMLDivElement | null) => {
previewRef.current = el;
setPreviewMounted(el !== null);
}, []);
return (
<div className="flex flex-col w-full">
<BackendServiceApi show={showApiKeyModal} />
@@ -35,13 +76,29 @@ const ApiContent = ({ id, idKey }: { id?: string; idKey: string }) => {
</Button>
</div>
<section className="flex flex-col gap-2 pb-5 flex-1 min-h-0 overflow-auto mb-4">
<div style={{ position: 'relative' }}>
{tocVisible && <MarkdownToc content={apiDoc} />}
</div>
<MarkdownPreview
source={apiDoc}
wrapperElement={{ 'data-color-mode': isDarkTheme ? 'dark' : 'light' }}
></MarkdownPreview>
{tocVisible && previewMounted && <MarkdownToc container={previewRef} />}
{docReady ? (
<Suspense
fallback={
<div className="flex justify-center py-10">
<Loader2 className="size-5 animate-spin text-text-secondary" />
</div>
}
>
<div ref={setPreviewRef}>
<LazyMarkdownPreview
source={cleanDoc}
wrapperElement={{
'data-color-mode': isDarkTheme ? 'dark' : 'light',
}}
/>
</div>
</Suspense>
) : (
<div className="flex justify-center py-10">
<Loader2 className="size-5 animate-spin text-text-secondary" />
</div>
)}
</section>
<LangfuseCard></LangfuseCard>
{apiKeyVisible && (

View File

@@ -2,23 +2,26 @@ import React, { useEffect, useState } from 'react';
import Anchor, { AnchorItem } from './anchor';
interface MarkdownTocProps {
content: string;
// A ref to the container that wraps the markdown preview this TOC tracks.
// Headings are queried only inside this element so the TOC can never pick up
// headings rendered by another markdown instance.
container: React.RefObject<HTMLElement>;
}
const MarkdownToc: React.FC<MarkdownTocProps> = ({ content }) => {
const MarkdownToc: React.FC<MarkdownTocProps> = ({ container }) => {
const [items, setItems] = useState<AnchorItem[]>([]);
useEffect(() => {
const generateTocItems = () => {
const headings = document.querySelectorAll(
const root = container.current;
if (!root) return;
let active = true;
const buildToc = () => {
const headings = root.querySelectorAll(
'.wmde-markdown h2, .wmde-markdown h3',
);
// If headings haven't rendered yet, wait for next frame
if (headings.length === 0) {
requestAnimationFrame(generateTocItems);
return;
}
if (headings.length === 0) return false;
const tocItems: AnchorItem[] = [];
let currentH2Item: AnchorItem | null = null;
@@ -51,14 +54,26 @@ const MarkdownToc: React.FC<MarkdownTocProps> = ({ content }) => {
}
});
setItems(tocItems.slice(1));
if (active) setItems(tocItems.slice(1));
return true;
};
// Use requestAnimationFrame to ensure execution after DOM rendering
requestAnimationFrame(() => {
requestAnimationFrame(generateTocItems);
// Build immediately if the preview already rendered its headings.
if (buildToc()) return;
// Otherwise wait for the lazy preview to inject its DOM, then build once.
// This replaces the previous per-frame requestAnimationFrame loop, which
// polled forever when the lazy chunk failed to load and leaked on unmount.
const observer = new MutationObserver(() => {
if (buildToc()) observer.disconnect();
});
}, [content]);
observer.observe(root, { childList: true, subtree: true });
return () => {
active = false;
observer.disconnect();
};
}, [container]);
return (
<div