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 { 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 { IModalProps } from '@/interfaces/common';
import { IToken } from '@/interfaces/database/chat';
import { formatDate } from '@/utils/date';
import { DeleteOutlined } from '@ant-design/icons';
import type { TableProps } from 'antd';
import { Button, Modal, Space, Table } from 'antd';
import { Trash2 } from 'lucide-react';
import { useOperateApiKey } from '../hooks';
const ChatApiKeyModal = ({
@ -17,57 +29,59 @@ const ChatApiKeyModal = ({
useOperateApiKey(idKey, dialogId);
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 (
<>
<Modal
title={t('apiKey')}
open
onCancel={hideModal}
cancelButtonProps={{ style: { display: 'none' } }}
style={{ top: 300 }}
onOk={hideModal}
width={'50vw'}
>
<Table
columns={columns}
dataSource={tokenList}
rowKey={'token'}
loading={listLoading}
pagination={false}
/>
<Button
onClick={createToken}
loading={creatingLoading}
disabled={tokenList?.length > 0}
>
{t('createNewKey')}
</Button>
</Modal>
<Dialog open onOpenChange={hideModal}>
<DialogContent className="max-w-[50vw]">
<DialogHeader>
<DialogTitle>{t('apiKey')}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{listLoading ? (
<div className="flex justify-center py-8">Loading...</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Token</TableHead>
<TableHead>{t('created')}</TableHead>
<TableHead>{t('action')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tokenList?.map((tokenItem) => (
<TableRow key={tokenItem.token}>
<TableCell className="font-medium break-all">
{tokenItem.token}
</TableCell>
<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 { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import { useSetModalState } from '@/hooks/common-hooks';
import { LangfuseCard } from '@/pages/user-setting/setting-model/langfuse';
import apiDoc from '@parent/docs/references/http_api_reference.md';
import MarkdownPreview from '@uiw/react-markdown-preview';
import { Button, Card, Flex, Space } from 'antd';
import ChatApiKeyModal from '../chat-api-key-modal';
import { usePreviewChat } from '../hooks';
import BackendServiceApi from './backend-service-api';
import MarkdownToc from './markdown-toc';
const ApiContent = ({
id,
idKey,
hideChatPreviewCard = false,
}: {
id?: string;
idKey: string;
hideChatPreviewCard?: boolean;
}) => {
const { t } = useTranslate('chat');
const ApiContent = ({ id, idKey }: { id?: string; idKey: string }) => {
const {
visible: apiKeyVisible,
hideModal: hideApiKeyModal,
showModal: showApiKeyModal,
} = useSetModalState();
// const { embedVisible, hideEmbedModal, showEmbedModal, embedToken } =
// useShowEmbedModal(idKey);
const { handlePreview } = usePreviewChat(idKey);
const isDarkTheme = useIsDarkTheme();
return (
<div className="pb-2">
<Flex vertical gap={'middle'}>
<section className="flex flex-col gap-2 pb-5">
<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' }}>
<MarkdownToc content={apiDoc} />
</div>
@ -54,7 +28,8 @@ const ApiContent = ({
source={apiDoc}
wrapperElement={{ 'data-color-mode': isDarkTheme ? 'dark' : 'light' }}
></MarkdownPreview>
</Flex>
</section>
<LangfuseCard></LangfuseCard>
{apiKeyVisible && (
<ChatApiKeyModal
hideModal={hideApiKeyModal}
@ -62,14 +37,6 @@ const ApiContent = ({
idKey={idKey}
></ChatApiKeyModal>
)}
{/* {embedVisible && (
<EmbedModal
token={embedToken}
visible={embedVisible}
hideModal={hideEmbedModal}
></EmbedModal>
)} */}
<LangfuseCard></LangfuseCard>
</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 styles from './index.less';
const { Paragraph } = Typography;
const BackendServiceApi = ({ show }: { show(): void }) => {
const { t } = useTranslate('chat');
return (
<Card
title={
<Space size={'large'}>
<span>RAGFlow API</span>
<Button onClick={show} type="primary">
{t('apiKey')}
</Button>
</Space>
}
>
<Flex gap={8} align="center">
<b>{t('backendServiceApi')}</b>
<Paragraph
copyable={{ text: `${location.origin}` }}
className={styles.apiLinkText}
>
{location.origin}
</Paragraph>
</Flex>
<Card>
<CardHeader>
<div className="flex items-center gap-4">
<CardTitle>RAGFlow API</CardTitle>
<Button onClick={show}>{t('apiKey')}</Button>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2">
<b className="font-semibold">{t('backendServiceApi')}</b>
<CopyToClipboardWithText
text={location.origin}
></CopyToClipboardWithText>
</div>
</CardContent>
</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 Anchor, { AnchorItem } from './anchor';
interface MarkdownTocProps {
content: string;
}
const MarkdownToc: React.FC<MarkdownTocProps> = ({ content }) => {
const [items, setItems] = useState<AnchorLinkItemProps[]>([]);
const [items, setItems] = useState<AnchorItem[]>([]);
useEffect(() => {
const generateTocItems = () => {
const headings = document.querySelectorAll(
'.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) => {
const title = heading.textContent || '';
@ -23,7 +29,7 @@ const MarkdownToc: React.FC<MarkdownTocProps> = ({ content }) => {
const isH2 = heading.tagName.toLowerCase() === 'h2';
if (id && title) {
const item: AnchorLinkItemProps = {
const item: AnchorItem = {
key: id,
href: `#${id}`,
title,
@ -48,7 +54,10 @@ const MarkdownToc: React.FC<MarkdownTocProps> = ({ content }) => {
setItems(tocItems.slice(1));
};
setTimeout(generateTocItems, 100);
// Use requestAnimationFrame to ensure execution after DOM rendering
requestAnimationFrame(() => {
requestAnimationFrame(generateTocItems);
});
}, [content]);
return (
@ -56,7 +65,7 @@ const MarkdownToc: React.FC<MarkdownTocProps> = ({ content }) => {
className="markdown-toc bg-bg-base text-text-primary shadow shadow-text-secondary"
style={{
position: 'fixed',
right: 20,
right: 30,
top: 100,
bottom: 150,
width: 200,
@ -66,7 +75,7 @@ const MarkdownToc: React.FC<MarkdownTocProps> = ({ content }) => {
zIndex: 1000,
}}
>
<Anchor items={items} affix={false} />
<Anchor items={items} />
</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 {
useSetModalState,
useShowDeleteConfirm,
@ -80,11 +79,6 @@ export const useShowBetaEmptyError = () => {
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 { showTokenEmptyError } = useShowTokenEmptyError();
const { showBetaEmptyError } = useShowBetaEmptyError();
@ -149,31 +143,3 @@ export const useShowEmbedModal = () => {
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 = () => {
return (
<div className={styles.apiWrapper}>
<ApiContent idKey="dialogId" hideChatPreviewCard></ApiContent>
<ApiContent idKey="dialogId"></ApiContent>
</div>
);
};

View File

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