mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-24 17:36:47 +08:00
fix: strip YAML frontmatter and defer API reference markdown rendering (#17272)
This commit is contained in:
@@ -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 && (
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user