Feat: Refactoring the documentation page using shadcn. #10427 (#12376)

### What problem does this PR solve?

Feat: Refactoring the documentation page using shadcn. #10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-12-31 19:00:37 +08:00
committed by GitHub
parent 96810b7d97
commit 10c28c5ecd
11 changed files with 204 additions and 386 deletions

View File

@ -1,11 +1,23 @@
import CopyToClipboard from '@/components/copy-to-clipboard'; import CopyToClipboard from '@/components/copy-to-clipboard';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common'; import { IModalProps } from '@/interfaces/common';
import { IToken } from '@/interfaces/database/chat';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { DeleteOutlined } from '@ant-design/icons'; import { Trash2 } from 'lucide-react';
import type { TableProps } from 'antd';
import { Button, Modal, Space, Table } from 'antd';
import { useOperateApiKey } from '../hooks'; import { useOperateApiKey } from '../hooks';
const ChatApiKeyModal = ({ const ChatApiKeyModal = ({
@ -17,57 +29,59 @@ const ChatApiKeyModal = ({
useOperateApiKey(idKey, dialogId); useOperateApiKey(idKey, dialogId);
const { t } = useTranslate('chat'); const { t } = useTranslate('chat');
const columns: TableProps<IToken>['columns'] = [
{
title: 'Token',
dataIndex: 'token',
key: 'token',
render: (text) => <a>{text}</a>,
},
{
title: t('created'),
dataIndex: 'create_date',
key: 'create_date',
render: (text) => formatDate(text),
},
{
title: t('action'),
key: 'action',
render: (_, record) => (
<Space size="middle">
<CopyToClipboard text={record.token}></CopyToClipboard>
<DeleteOutlined onClick={() => removeToken(record.token)} />
</Space>
),
},
];
return ( return (
<> <>
<Modal <Dialog open onOpenChange={hideModal}>
title={t('apiKey')} <DialogContent className="max-w-[50vw]">
open <DialogHeader>
onCancel={hideModal} <DialogTitle>{t('apiKey')}</DialogTitle>
cancelButtonProps={{ style: { display: 'none' } }} </DialogHeader>
style={{ top: 300 }} <div className="space-y-4">
onOk={hideModal} {listLoading ? (
width={'50vw'} <div className="flex justify-center py-8">Loading...</div>
> ) : (
<Table <Table>
columns={columns} <TableHeader>
dataSource={tokenList} <TableRow>
rowKey={'token'} <TableHead>Token</TableHead>
loading={listLoading} <TableHead>{t('created')}</TableHead>
pagination={false} <TableHead>{t('action')}</TableHead>
/> </TableRow>
<Button </TableHeader>
onClick={createToken} <TableBody>
loading={creatingLoading} {tokenList?.map((tokenItem) => (
disabled={tokenList?.length > 0} <TableRow key={tokenItem.token}>
> <TableCell className="font-medium break-all">
{t('createNewKey')} {tokenItem.token}
</Button> </TableCell>
</Modal> <TableCell>{formatDate(tokenItem.create_date)}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<CopyToClipboard text={tokenItem.token} />
<Button
variant="ghost"
size="icon"
onClick={() => removeToken(tokenItem.token)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
<Button
onClick={createToken}
loading={creatingLoading}
disabled={tokenList?.length > 0}
>
{t('createNewKey')}
</Button>
</div>
</DialogContent>
</Dialog>
</> </>
); );
}; };

View File

@ -0,0 +1,93 @@
import React, { useSyncExternalStore } from 'react';
export interface AnchorItem {
key: string;
href: string;
title: string;
children?: AnchorItem[];
}
interface SimpleAnchorProps {
items: AnchorItem[];
className?: string;
style?: React.CSSProperties;
}
// Subscribe to URL hash changes
const subscribeHash = (callback: () => void) => {
window.addEventListener('hashchange', callback);
return () => window.removeEventListener('hashchange', callback);
};
const getHash = () => window.location.hash;
const Anchor: React.FC<SimpleAnchorProps> = ({
items,
className = '',
style = {},
}) => {
// Sync with URL hash changes, to highlight the active item
const hash = useSyncExternalStore(subscribeHash, getHash);
// Handle menu item click
const handleClick = (
e: React.MouseEvent<HTMLAnchorElement>,
href: string,
) => {
e.preventDefault();
const targetId = href.replace('#', '');
const targetElement = document.getElementById(targetId);
if (targetElement) {
// Update URL hash (triggers hashchange event)
window.location.hash = href;
// Smooth scroll to target
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
if (items.length === 0) return null;
return (
<nav className={className} style={style}>
<ul className="list-none p-0 m-0">
{items.map((item) => (
<li key={item.key} className="mb-2">
<a
href={item.href}
onClick={(e) => handleClick(e, item.href)}
className={`block px-3 py-1.5 no-underline rounded cursor-pointer transition-all duration-300 hover:text-accent-primary/70 ${
hash === item.href
? 'text-accent-primary bg-accent-primary-5'
: 'text-text-secondary bg-transparent'
}`}
>
{item.title}
</a>
{item.children && item.children.length > 0 && (
<ul className="list-none p-0 ml-4 mt-1">
{item.children.map((child) => (
<li key={child.key} className="mb-1">
<a
href={child.href}
onClick={(e) => handleClick(e, child.href)}
className={`block px-3 py-1 text-sm no-underline rounded cursor-pointer transition-all duration-300 hover:text-accent-primary/70 ${
hash === child.href
? 'text-accent-primary bg-accent-primary-5'
: 'text-text-secondary bg-transparent'
}`}
>
{child.title}
</a>
</li>
))}
</ul>
)}
</li>
))}
</ul>
</nav>
);
};
export default Anchor;

View File

@ -1,52 +1,26 @@
import { useIsDarkTheme } from '@/components/theme-provider'; import { useIsDarkTheme } from '@/components/theme-provider';
import { useSetModalState, useTranslate } from '@/hooks/common-hooks'; import { useSetModalState } from '@/hooks/common-hooks';
import { LangfuseCard } from '@/pages/user-setting/setting-model/langfuse'; import { LangfuseCard } from '@/pages/user-setting/setting-model/langfuse';
import apiDoc from '@parent/docs/references/http_api_reference.md'; import apiDoc from '@parent/docs/references/http_api_reference.md';
import MarkdownPreview from '@uiw/react-markdown-preview'; import MarkdownPreview from '@uiw/react-markdown-preview';
import { Button, Card, Flex, Space } from 'antd';
import ChatApiKeyModal from '../chat-api-key-modal'; import ChatApiKeyModal from '../chat-api-key-modal';
import { usePreviewChat } from '../hooks';
import BackendServiceApi from './backend-service-api'; import BackendServiceApi from './backend-service-api';
import MarkdownToc from './markdown-toc'; import MarkdownToc from './markdown-toc';
const ApiContent = ({ const ApiContent = ({ id, idKey }: { id?: string; idKey: string }) => {
id,
idKey,
hideChatPreviewCard = false,
}: {
id?: string;
idKey: string;
hideChatPreviewCard?: boolean;
}) => {
const { t } = useTranslate('chat');
const { const {
visible: apiKeyVisible, visible: apiKeyVisible,
hideModal: hideApiKeyModal, hideModal: hideApiKeyModal,
showModal: showApiKeyModal, showModal: showApiKeyModal,
} = useSetModalState(); } = useSetModalState();
// const { embedVisible, hideEmbedModal, showEmbedModal, embedToken } =
// useShowEmbedModal(idKey);
const { handlePreview } = usePreviewChat(idKey);
const isDarkTheme = useIsDarkTheme(); const isDarkTheme = useIsDarkTheme();
return ( return (
<div className="pb-2"> <div className="pb-2">
<Flex vertical gap={'middle'}> <section className="flex flex-col gap-2 pb-5">
<BackendServiceApi show={showApiKeyModal}></BackendServiceApi> <BackendServiceApi show={showApiKeyModal}></BackendServiceApi>
{!hideChatPreviewCard && (
<Card title={`${name} Web App`}>
<Flex gap={8} vertical>
<Space size={'middle'}>
<Button onClick={handlePreview}>{t('preview')}</Button>
{/* <Button onClick={() => showEmbedModal(id)}>
{t('embedded')}
</Button> */}
</Space>
</Flex>
</Card>
)}
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<MarkdownToc content={apiDoc} /> <MarkdownToc content={apiDoc} />
</div> </div>
@ -54,7 +28,8 @@ const ApiContent = ({
source={apiDoc} source={apiDoc}
wrapperElement={{ 'data-color-mode': isDarkTheme ? 'dark' : 'light' }} wrapperElement={{ 'data-color-mode': isDarkTheme ? 'dark' : 'light' }}
></MarkdownPreview> ></MarkdownPreview>
</Flex> </section>
<LangfuseCard></LangfuseCard>
{apiKeyVisible && ( {apiKeyVisible && (
<ChatApiKeyModal <ChatApiKeyModal
hideModal={hideApiKeyModal} hideModal={hideApiKeyModal}
@ -62,14 +37,6 @@ const ApiContent = ({
idKey={idKey} idKey={idKey}
></ChatApiKeyModal> ></ChatApiKeyModal>
)} )}
{/* {embedVisible && (
<EmbedModal
token={embedToken}
visible={embedVisible}
hideModal={hideEmbedModal}
></EmbedModal>
)} */}
<LangfuseCard></LangfuseCard>
</div> </div>
); );
}; };

View File

@ -1,33 +1,28 @@
import { Button, Card, Flex, Space, Typography } from 'antd'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { CopyToClipboardWithText } from '@/components/copy-to-clipboard';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import styles from './index.less';
const { Paragraph } = Typography;
const BackendServiceApi = ({ show }: { show(): void }) => { const BackendServiceApi = ({ show }: { show(): void }) => {
const { t } = useTranslate('chat'); const { t } = useTranslate('chat');
return ( return (
<Card <Card>
title={ <CardHeader>
<Space size={'large'}> <div className="flex items-center gap-4">
<span>RAGFlow API</span> <CardTitle>RAGFlow API</CardTitle>
<Button onClick={show} type="primary"> <Button onClick={show}>{t('apiKey')}</Button>
{t('apiKey')} </div>
</Button> </CardHeader>
</Space> <CardContent>
} <div className="flex items-center gap-2">
> <b className="font-semibold">{t('backendServiceApi')}</b>
<Flex gap={8} align="center"> <CopyToClipboardWithText
<b>{t('backendServiceApi')}</b> text={location.origin}
<Paragraph ></CopyToClipboardWithText>
copyable={{ text: `${location.origin}` }} </div>
className={styles.apiLinkText} </CardContent>
>
{location.origin}
</Paragraph>
</Flex>
</Card> </Card>
); );
}; };

View File

@ -1,31 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { Modal } from 'antd';
import ApiContent from './api-content';
const ChatOverviewModal = ({
visible,
hideModal,
id,
idKey,
}: IModalProps<any> & { id: string; name?: string; idKey: string }) => {
const { t } = useTranslate('chat');
return (
<>
<Modal
title={t('overview')}
open={visible}
onCancel={hideModal}
cancelButtonProps={{ style: { display: 'none' } }}
onOk={hideModal}
width={'100vw'}
okText={t('close', { keyPrefix: 'common' })}
>
<ApiContent id={id} idKey={idKey}></ApiContent>
</Modal>
</>
);
};
export default ChatOverviewModal;

View File

@ -1,21 +1,27 @@
import { Anchor } from 'antd';
import type { AnchorLinkItemProps } from 'antd/es/anchor/Anchor';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Anchor, { AnchorItem } from './anchor';
interface MarkdownTocProps { interface MarkdownTocProps {
content: string; content: string;
} }
const MarkdownToc: React.FC<MarkdownTocProps> = ({ content }) => { const MarkdownToc: React.FC<MarkdownTocProps> = ({ content }) => {
const [items, setItems] = useState<AnchorLinkItemProps[]>([]); const [items, setItems] = useState<AnchorItem[]>([]);
useEffect(() => { useEffect(() => {
const generateTocItems = () => { const generateTocItems = () => {
const headings = document.querySelectorAll( const headings = document.querySelectorAll(
'.wmde-markdown h2, .wmde-markdown h3', '.wmde-markdown h2, .wmde-markdown h3',
); );
const tocItems: AnchorLinkItemProps[] = [];
let currentH2Item: AnchorLinkItemProps | null = null; // If headings haven't rendered yet, wait for next frame
if (headings.length === 0) {
requestAnimationFrame(generateTocItems);
return;
}
const tocItems: AnchorItem[] = [];
let currentH2Item: AnchorItem | null = null;
headings.forEach((heading) => { headings.forEach((heading) => {
const title = heading.textContent || ''; const title = heading.textContent || '';
@ -23,7 +29,7 @@ const MarkdownToc: React.FC<MarkdownTocProps> = ({ content }) => {
const isH2 = heading.tagName.toLowerCase() === 'h2'; const isH2 = heading.tagName.toLowerCase() === 'h2';
if (id && title) { if (id && title) {
const item: AnchorLinkItemProps = { const item: AnchorItem = {
key: id, key: id,
href: `#${id}`, href: `#${id}`,
title, title,
@ -48,7 +54,10 @@ const MarkdownToc: React.FC<MarkdownTocProps> = ({ content }) => {
setItems(tocItems.slice(1)); setItems(tocItems.slice(1));
}; };
setTimeout(generateTocItems, 100); // Use requestAnimationFrame to ensure execution after DOM rendering
requestAnimationFrame(() => {
requestAnimationFrame(generateTocItems);
});
}, [content]); }, [content]);
return ( return (
@ -56,7 +65,7 @@ const MarkdownToc: React.FC<MarkdownTocProps> = ({ content }) => {
className="markdown-toc bg-bg-base text-text-primary shadow shadow-text-secondary" className="markdown-toc bg-bg-base text-text-primary shadow shadow-text-secondary"
style={{ style={{
position: 'fixed', position: 'fixed',
right: 20, right: 30,
top: 100, top: 100,
bottom: 150, bottom: 150,
width: 200, width: 200,
@ -66,7 +75,7 @@ const MarkdownToc: React.FC<MarkdownTocProps> = ({ content }) => {
zIndex: 1000, zIndex: 1000,
}} }}
> >
<Anchor items={items} affix={false} /> <Anchor items={items} />
</div> </div>
); );
}; };

View File

@ -1,21 +0,0 @@
.codeCard {
.clearCardBody();
}
.codeText {
padding: 10px;
background-color: #ffffff09;
}
.id {
.linkText();
}
.darkBg {
background-color: rgb(69, 68, 68);
}
.darkId {
color: white;
.darkBg();
}

View File

@ -1,170 +0,0 @@
import CopyToClipboard from '@/components/copy-to-clipboard';
import HighLightMarkdown from '@/components/highlight-markdown';
import { SharedFrom } from '@/constants/chat';
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import {
Card,
Checkbox,
Form,
Modal,
Select,
Tabs,
TabsProps,
Typography,
} from 'antd';
import { useMemo, useState } from 'react';
import { useIsDarkTheme } from '@/components/theme-provider';
import {
LanguageAbbreviation,
LanguageAbbreviationMap,
} from '@/constants/common';
import { cn } from '@/lib/utils';
import styles from './index.less';
const { Paragraph, Link } = Typography;
const EmbedModal = ({
visible,
hideModal,
token = '',
form,
beta = '',
isAgent,
}: IModalProps<any> & {
token: string;
form: SharedFrom;
beta: string;
isAgent: boolean;
}) => {
const { t } = useTranslate('chat');
const isDarkTheme = useIsDarkTheme();
const [visibleAvatar, setVisibleAvatar] = useState(false);
const [locale, setLocale] = useState('');
const languageOptions = useMemo(() => {
return Object.values(LanguageAbbreviation).map((x) => ({
label: LanguageAbbreviationMap[x],
value: x,
}));
}, []);
const generateIframeSrc = () => {
let src = `${location.origin}/chat/share?shared_id=${token}&from=${form}&auth=${beta}`;
if (visibleAvatar) {
src += '&visible_avatar=1';
}
if (locale) {
src += `&locale=${locale}`;
}
return src;
};
const iframeSrc = generateIframeSrc();
const text = `
~~~ html
<iframe
src="${iframeSrc}"
style="width: 100%; height: 100%; min-height: 600px"
frameborder="0"
>
</iframe>
~~~
`;
const items: TabsProps['items'] = [
{
key: '1',
label: t('fullScreenTitle'),
children: (
<Card
title={t('fullScreenDescription')}
extra={<CopyToClipboard text={text}></CopyToClipboard>}
className={styles.codeCard}
>
<div className="p-2">
<h2 className="mb-3">Option:</h2>
<Form.Item
label={t('avatarHidden')}
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
>
<Checkbox
checked={visibleAvatar}
onChange={(e) => setVisibleAvatar(e.target.checked)}
></Checkbox>
</Form.Item>
<Form.Item
label={t('locale')}
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
>
<Select
placeholder="Select a locale"
onChange={(value) => setLocale(value)}
options={languageOptions}
style={{ width: '100%' }}
/>
</Form.Item>
</div>
<HighLightMarkdown>{text}</HighLightMarkdown>
</Card>
),
},
{
key: '2',
label: t('partialTitle'),
children: t('comingSoon'),
},
{
key: '3',
label: t('extensionTitle'),
children: t('comingSoon'),
},
];
const onChange = (key: string) => {
console.log(key);
};
return (
<Modal
title={t('embedIntoSite', { keyPrefix: 'common' })}
open={visible}
style={{ top: 300 }}
width={'50vw'}
onOk={hideModal}
onCancel={hideModal}
>
<Tabs defaultActiveKey="1" items={items} onChange={onChange} />
<div className="text-base font-medium mt-4 mb-1">
{t(isAgent ? 'flow' : 'chat', { keyPrefix: 'header' })}
<span className="ml-1 inline-block">ID</span>
</div>
<Paragraph
copyable={{ text: token }}
className={cn(styles.id, {
[styles.darkId]: isDarkTheme,
})}
>
{token}
</Paragraph>
<Link
href={
isAgent
? 'https://ragflow.io/docs/dev/http_api_reference#create-session-with-agent'
: 'https://ragflow.io/docs/dev/http_api_reference#create-session-with-chat-assistant'
}
target="_blank"
>
{t('howUseId', { keyPrefix: isAgent ? 'flow' : 'chat' })}
</Link>
</Modal>
);
};
export default EmbedModal;

View File

@ -1,4 +1,3 @@
import { SharedFrom } from '@/constants/chat';
import { import {
useSetModalState, useSetModalState,
useShowDeleteConfirm, useShowDeleteConfirm,
@ -80,11 +79,6 @@ export const useShowBetaEmptyError = () => {
return { showBetaEmptyError }; return { showBetaEmptyError };
}; };
const getUrlWithToken = (token: string, from: string = 'chat') => {
const { protocol, host } = window.location;
return `${protocol}//${host}/chat/share?shared_id=${token}&from=${from}`;
};
const useFetchTokenListBeforeOtherStep = () => { const useFetchTokenListBeforeOtherStep = () => {
const { showTokenEmptyError } = useShowTokenEmptyError(); const { showTokenEmptyError } = useShowTokenEmptyError();
const { showBetaEmptyError } = useShowBetaEmptyError(); const { showBetaEmptyError } = useShowBetaEmptyError();
@ -149,31 +143,3 @@ export const useShowEmbedModal = () => {
beta, beta,
}; };
}; };
export const usePreviewChat = (idKey: string) => {
const { handleOperate } = useFetchTokenListBeforeOtherStep();
const open = useCallback(
(t: string) => {
window.open(
getUrlWithToken(
t,
idKey === 'canvasId' ? SharedFrom.Agent : SharedFrom.Chat,
),
'_blank',
);
},
[idKey],
);
const handlePreview = useCallback(async () => {
const token = await handleOperate();
if (token) {
open(token);
}
}, [handleOperate, open]);
return {
handlePreview,
};
};

View File

@ -5,7 +5,7 @@ import styles from './index.less';
const ApiPage = () => { const ApiPage = () => {
return ( return (
<div className={styles.apiWrapper}> <div className={styles.apiWrapper}>
<ApiContent idKey="dialogId" hideChatPreviewCard></ApiContent> <ApiContent idKey="dialogId"></ApiContent>
</div> </div>
); );
}; };

View File

@ -45,11 +45,7 @@ export function LangfuseCard() {
<Eye /> {t('setting.view')} <Eye /> {t('setting.view')}
</Button> </Button>
)} )}
<Button <Button size={'sm'} onClick={showSaveLangfuseConfigurationModal}>
size={'sm'}
onClick={showSaveLangfuseConfigurationModal}
className="bg-blue-500 hover:bg-blue-400"
>
<Settings2 /> <Settings2 />
{t('setting.configuration')} {t('setting.configuration')}
</Button> </Button>