From 91d9bf7fc499c6fb0843ebe09a741a8943ff8641 Mon Sep 17 00:00:00 2001 From: euvre <93761161+euvre@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:37:10 +0800 Subject: [PATCH] fix: strip YAML frontmatter and defer API reference markdown rendering (#17272) --- .../chat-overview-modal/api-content.tsx | 73 +++++++++++++++++-- .../chat-overview-modal/markdown-toc.tsx | 45 ++++++++---- 2 files changed, 95 insertions(+), 23 deletions(-) diff --git a/web/src/components/api-service/chat-overview-modal/api-content.tsx b/web/src/components/api-service/chat-overview-modal/api-content.tsx index b7127f53e9..972ae40f0d 100644 --- a/web/src/components/api-service/chat-overview-modal/api-content.tsx +++ b/web/src/components/api-service/chat-overview-modal/api-content.tsx @@ -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(null); + const [previewMounted, setPreviewMounted] = useState(false); + const setPreviewRef = useCallback((el: HTMLDivElement | null) => { + previewRef.current = el; + setPreviewMounted(el !== null); + }, []); + return (
@@ -35,13 +76,29 @@ const ApiContent = ({ id, idKey }: { id?: string; idKey: string }) => {
-
- {tocVisible && } -
- + {tocVisible && previewMounted && } + {docReady ? ( + + + + } + > +
+ +
+
+ ) : ( +
+ +
+ )}
{apiKeyVisible && ( diff --git a/web/src/components/api-service/chat-overview-modal/markdown-toc.tsx b/web/src/components/api-service/chat-overview-modal/markdown-toc.tsx index 3141b54960..4f06ab578f 100644 --- a/web/src/components/api-service/chat-overview-modal/markdown-toc.tsx +++ b/web/src/components/api-service/chat-overview-modal/markdown-toc.tsx @@ -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; } -const MarkdownToc: React.FC = ({ content }) => { +const MarkdownToc: React.FC = ({ container }) => { const [items, setItems] = useState([]); 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 = ({ 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 (