diff --git a/web/src/pages/chat/share/shared-markdown.tsx b/web/src/components/highlight-markdown/index.tsx similarity index 84% rename from web/src/pages/chat/share/shared-markdown.tsx rename to web/src/components/highlight-markdown/index.tsx index 2c1a3c040b5f5b14be8fb68da5a36148461136fa..7c393fa5c1f020011149b5124a984e1adfe9ca6e 100644 --- a/web/src/pages/chat/share/shared-markdown.tsx +++ b/web/src/components/highlight-markdown/index.tsx @@ -2,7 +2,11 @@ import Markdown from 'react-markdown'; import SyntaxHighlighter from 'react-syntax-highlighter'; import remarkGfm from 'remark-gfm'; -const SharedMarkdown = ({ content }: { content: string }) => { +const HightLightMarkdown = ({ + children, +}: { + children: string | null | undefined; +}) => { return ( <Markdown remarkPlugins={[remarkGfm]} @@ -24,9 +28,9 @@ const SharedMarkdown = ({ content }: { content: string }) => { } as any } > - {content} + {children} </Markdown> ); }; -export default SharedMarkdown; +export default HightLightMarkdown; diff --git a/web/src/hooks/chatHooks.ts b/web/src/hooks/chatHooks.ts index 3f33f6b0a64a1c44588f2dd406112263062699ab..34e0d74b0d80f243a2a177b7e5ea7c23dbe7e632 100644 --- a/web/src/hooks/chatHooks.ts +++ b/web/src/hooks/chatHooks.ts @@ -4,7 +4,7 @@ import { IStats, IToken, } from '@/interfaces/database/chat'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback } from 'react'; import { useDispatch, useSelector } from 'umi'; export const useFetchDialogList = () => { @@ -299,27 +299,4 @@ export const useCompleteSharedConversation = () => { return completeSharedConversation; }; -export const useCreatePublicUrlToken = (dialogId: string, visible: boolean) => { - const [token, setToken] = useState(); - const createToken = useCreateToken(dialogId); - const { protocol, host } = window.location; - - const urlWithToken = `${protocol}//${host}/chat/share?shared_id=${token}`; - - const createUrlToken = useCallback(async () => { - if (visible) { - const data = await createToken(); - const urlToken = data.data?.token; - if (urlToken) { - setToken(urlToken); - } - } - }, [createToken, visible]); - - useEffect(() => { - createUrlToken(); - }, [createUrlToken]); - - return { token, createUrlToken, urlWithToken }; -}; //#endregion diff --git a/web/src/less/mixins.less b/web/src/less/mixins.less index b5256f7e0f8ccd36e831a5d89b1c7d6eeb1e02c8..ba363ec456d210de760abaa601946978a36b2199 100644 --- a/web/src/less/mixins.less +++ b/web/src/less/mixins.less @@ -33,3 +33,12 @@ .pointerCursor() { cursor: pointer; } + +.clearCardBody() { + :global { + .ant-card-body { + padding: 0; + margin: 0; + } + } +} diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index dafe2a07ac80a2bd97e9d21202e05975524275b7..ea3f507b2628959b1db560546001b899bcaca178 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -349,7 +349,7 @@ export default { 'This sets the maximum length of the model’s output, measured in the number of tokens (words or pieces of words).', quote: 'Show Quote', quoteTip: 'Should the source of the original text be displayed?', - overview: 'Overview', + overview: 'API', pv: 'Number of messages', uv: 'Active user number', speed: 'Token output speed', @@ -367,6 +367,14 @@ export default { createNewKey: 'Create new key', created: 'Created', action: 'Action', + embedModalTitle: 'Embed into website', + comingSoon: 'Coming Soon', + fullScreenTitle: 'Full Embed', + fullScreenDescription: + 'Embed the following iframe into your website at the desired location', + partialTitle: 'Partial Embed', + extensionTitle: 'Chrome Extension', + tokenError: 'Please create API Token first!', }, setting: { profile: 'Profile', diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index f469759ca35af988dbd6034a8262bca5c2effbe6..73c150aab5d47bf932bb8540e17385ece1eb07ae 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -321,7 +321,7 @@ export default { '這č¨ç˝®äş†ć¨ˇĺž‹čĽ¸ĺ‡şçš„最大長度,以標č¨ďĽĺ–®č©žć–單詞片段)的數量來衡量。', quote: '顯示引文', quoteTip: 'ćŻĺ¦ć‡‰č©˛éˇŻç¤şĺŽźć–‡ĺ‡şč™•ďĽź', - overview: '概覽', + overview: 'API', pv: 'ć¶ćŻć•¸', uv: '活躍用ć¶ć•¸', speed: 'Token 輸出速度', @@ -339,6 +339,13 @@ export default { createNewKey: '創建新密鑰', created: '創建於', action: '操作', + embedModalTitle: '嵌入網站', + comingSoon: '即將推出', + fullScreenTitle: '全屏嵌入', + fullScreenDescription: '將以下iframe嵌入您的網站處於所需位置', + partialTitle: 'é¨ĺ†ĺµŚĺ…Ą', + extensionTitle: 'Chrome 插件', + tokenError: 'č«‹ĺ…創建 Api Token!', }, setting: { profile: '概述', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 6d662342f5ea7bd76d2f048dbaaf4e5232905e35..b3793030c46fe6a1e5044fa02913cb3c3efcc68e 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -338,7 +338,7 @@ export default { 'čż™č®ľç˝®äş†ć¨ˇĺž‹čľ“ĺ‡şçš„ćś€ĺ¤§é•żĺş¦ďĽŚä»Ąć ‡č®°ďĽĺŤ•čŻŤć–单词片段)的数量来衡量。', quote: 'ćľç¤şĺĽ•ć–‡', quoteTip: 'ćŻĺ¦ĺş”该ćľç¤şĺŽźć–‡ĺ‡şĺ¤„?', - overview: '概č§', + overview: 'API', pv: 'ć¶ćŻć•°', uv: 'ć´»č·ç”¨ć·ć•°', speed: 'Token 输出速度', @@ -356,6 +356,13 @@ export default { createNewKey: 'ĺ›ĺ»şć–°ĺŻ†é’Ą', created: 'ĺ›ĺ»şäşŽ', action: '操作', + embedModalTitle: '嵌入网站', + comingSoon: '即将推出', + fullScreenTitle: '全屏嵌入', + fullScreenDescription: '将以下iframe嵌入您的网站处于所需位置', + partialTitle: 'é¨ĺ†ĺµŚĺ…Ą', + extensionTitle: 'Chrome 插件', + tokenError: '请ĺ…ĺ›ĺ»ş Api Token!', }, setting: { profile: '概č¦', diff --git a/web/src/pages/chat/chat-overview-modal/index.tsx b/web/src/pages/chat/chat-overview-modal/index.tsx index c8a1c4126b599c13488edde8984df23f2f40d9d6..6381d65616075af9464684ff85c41587f57f3219 100644 --- a/web/src/pages/chat/chat-overview-modal/index.tsx +++ b/web/src/pages/chat/chat-overview-modal/index.tsx @@ -1,17 +1,19 @@ -import CopyToClipboard from '@/components/copy-to-clipboard'; import LineChart from '@/components/line-chart'; -import { useCreatePublicUrlToken } from '@/hooks/chatHooks'; import { useSetModalState, useTranslate } from '@/hooks/commonHooks'; import { IModalProps } from '@/interfaces/common'; import { IDialog, IStats } from '@/interfaces/database/chat'; -import { ReloadOutlined } from '@ant-design/icons'; import { Button, Card, DatePicker, Flex, Modal, Space, Typography } from 'antd'; import { RangePickerProps } from 'antd/es/date-picker'; import dayjs from 'dayjs'; import camelCase from 'lodash/camelCase'; -import { Link } from 'umi'; import ChatApiKeyModal from '../chat-api-key-modal'; -import { useFetchStatsOnMount, useSelectChartStatsList } from '../hooks'; +import EmbedModal from '../embed-modal'; +import { + useFetchStatsOnMount, + usePreviewChat, + useSelectChartStatsList, + useShowEmbedModal, +} from '../hooks'; import styles from './index.less'; const { Paragraph } = Typography; @@ -24,16 +26,18 @@ const ChatOverviewModal = ({ }: IModalProps<any> & { dialog: IDialog }) => { const { t } = useTranslate('chat'); const chartList = useSelectChartStatsList(); - const { urlWithToken, createUrlToken, token } = useCreatePublicUrlToken( - dialog.id, - visible, - ); - const { visible: apiKeyVisible, hideModal: hideApiKeyModal, showModal: showApiKeyModal, } = useSetModalState(); + const { + embedVisible, + hideEmbedModal, + showEmbedModal, + embedToken, + errorContextHolder, + } = useShowEmbedModal(dialog.id); const { pickerValue, setPickerValue } = useFetchStatsOnMount(visible); @@ -41,6 +45,8 @@ const ChatOverviewModal = ({ return current && current > dayjs().endOf('day'); }; + const { handlePreview, contextHolder } = usePreviewChat(dialog.id); + return ( <> <Modal @@ -50,36 +56,41 @@ const ChatOverviewModal = ({ width={'100vw'} > <Flex vertical gap={'middle'}> - <Card title={dialog.name}> - <Flex gap={8} vertical> - {t('publicUrl')} - <Flex className={styles.linkText} gap={10}> - <span>{urlWithToken}</span> - <CopyToClipboard text={urlWithToken}></CopyToClipboard> - <ReloadOutlined onClick={createUrlToken} /> - </Flex> - <Space size={'middle'}> - <Button> - <Link to={`/chat/share?shared_id=${token}`} target="_blank"> - {t('preview')} - </Link> - </Button> - <Button>{t('embedded')}</Button> - </Space> - </Flex> - </Card> <Card title={t('backendServiceApi')}> <Flex gap={8} vertical> {t('serviceApiEndpoint')} <Paragraph copyable className={styles.linkText}> - This is a copyable text. + https://demo.ragflow.io/v1/api/ </Paragraph> </Flex> <Space size={'middle'}> <Button onClick={showApiKeyModal}>{t('apiKey')}</Button> - <Button>{t('apiReference')}</Button> + <a + href={ + 'https://github.com/infiniflow/ragflow/blob/main/docs/conversation_api.md' + } + target="_blank" + rel="noreferrer" + > + <Button>{t('apiReference')}</Button> + </a> </Space> </Card> + <Card title={dialog.name}> + <Flex gap={8} vertical> + {t('publicUrl')} + {/* <Flex className={styles.linkText} gap={10}> + <span>{urlWithToken}</span> + <CopyToClipboard text={urlWithToken}></CopyToClipboard> + <ReloadOutlined onClick={createUrlToken} /> + </Flex> */} + <Space size={'middle'}> + <Button onClick={handlePreview}>{t('preview')}</Button> + <Button onClick={showEmbedModal}>{t('embedded')}</Button> + </Space> + </Flex> + </Card> + <Space> <b>{t('dateRange')}</b> <RangePicker @@ -103,6 +114,13 @@ const ChatOverviewModal = ({ hideModal={hideApiKeyModal} dialogId={dialog.id} ></ChatApiKeyModal> + <EmbedModal + token={embedToken} + visible={embedVisible} + hideModal={hideEmbedModal} + ></EmbedModal> + {contextHolder} + {errorContextHolder} </Modal> </> ); diff --git a/web/src/pages/chat/embed-modal/index.less b/web/src/pages/chat/embed-modal/index.less new file mode 100644 index 0000000000000000000000000000000000000000..5e807d8526efffbc85a64b26ae96d7eb20f85488 --- /dev/null +++ b/web/src/pages/chat/embed-modal/index.less @@ -0,0 +1,8 @@ +.codeCard { + .clearCardBody(); +} + +.codeText { + padding: 10px; + background-color: #e8e8ea; +} diff --git a/web/src/pages/chat/embed-modal/index.tsx b/web/src/pages/chat/embed-modal/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..44d3967650a9a386aac7abd171427a2342dbded7 --- /dev/null +++ b/web/src/pages/chat/embed-modal/index.tsx @@ -0,0 +1,70 @@ +import CopyToClipboard from '@/components/copy-to-clipboard'; +import HightLightMarkdown from '@/components/highlight-markdown'; +import { useTranslate } from '@/hooks/commonHooks'; +import { IModalProps } from '@/interfaces/common'; +import { Card, Modal, Tabs, TabsProps } from 'antd'; +import styles from './index.less'; + +const EmbedModal = ({ + visible, + hideModal, + token = '', +}: IModalProps<any> & { token: string }) => { + const { t } = useTranslate('chat'); + + const text = ` + ~~~ html + <iframe + src="https://demo.ragflow.io/chat/share?shared_id=${token}" + 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} + > + <HightLightMarkdown>{text}</HightLightMarkdown> + </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('embedModalTitle')} + open={visible} + style={{ top: 300 }} + width={'50vw'} + onOk={hideModal} + onCancel={hideModal} + > + <Tabs defaultActiveKey="1" items={items} onChange={onChange} /> + </Modal> + ); +}; + +export default EmbedModal; diff --git a/web/src/pages/chat/hooks.ts b/web/src/pages/chat/hooks.ts index cf5c858310d9c9b83ee00bc77875727b762457e1..e44155dea6a5466ec315664a29f24779a1834495 100644 --- a/web/src/pages/chat/hooks.ts +++ b/web/src/pages/chat/hooks.ts @@ -14,15 +14,21 @@ import { useRemoveToken, useSelectConversationList, useSelectDialogList, + useSelectStats, useSelectTokenList, useSetDialog, useUpdateConversation, } from '@/hooks/chatHooks'; -import { useSetModalState, useShowDeleteConfirm } from '@/hooks/commonHooks'; +import { + useSetModalState, + useShowDeleteConfirm, + useTranslate, +} from '@/hooks/commonHooks'; import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; import { IConversation, IDialog, IStats } from '@/interfaces/database/chat'; import { IChunk } from '@/interfaces/database/knowledge'; import { getFileExtension } from '@/utils'; +import { message } from 'antd'; import dayjs, { Dayjs } from 'dayjs'; import omit from 'lodash/omit'; import { @@ -777,35 +783,35 @@ type ChartStatsType = { }; export const useSelectChartStatsList = (): ChartStatsType => { - // const stats: IStats = useSelectStats(); - const stats = { - pv: [ - ['2024-06-01', 1], - ['2024-07-24', 3], - ['2024-09-01', 10], - ], - uv: [ - ['2024-02-01', 0], - ['2024-03-01', 99], - ['2024-05-01', 3], - ], - speed: [ - ['2024-09-01', 2], - ['2024-09-01', 3], - ], - tokens: [ - ['2024-09-01', 1], - ['2024-09-01', 3], - ], - round: [ - ['2024-09-01', 0], - ['2024-09-01', 3], - ], - thumb_up: [ - ['2024-09-01', 3], - ['2024-09-01', 9], - ], - }; + const stats: IStats = useSelectStats(); + // const stats = { + // pv: [ + // ['2024-06-01', 1], + // ['2024-07-24', 3], + // ['2024-09-01', 10], + // ], + // uv: [ + // ['2024-02-01', 0], + // ['2024-03-01', 99], + // ['2024-05-01', 3], + // ], + // speed: [ + // ['2024-09-01', 2], + // ['2024-09-01', 3], + // ], + // tokens: [ + // ['2024-09-01', 1], + // ['2024-09-01', 3], + // ], + // round: [ + // ['2024-09-01', 0], + // ['2024-09-01', 3], + // ], + // thumb_up: [ + // ['2024-09-01', 3], + // ['2024-09-01', 9], + // ], + // }; return Object.keys(stats).reduce((pre, cur) => { const item = stats[cur as keyof IStats]; @@ -819,4 +825,93 @@ export const useSelectChartStatsList = (): ChartStatsType => { }, {} as ChartStatsType); }; +export const useShowTokenEmptyError = () => { + const [messageApi, contextHolder] = message.useMessage(); + const { t } = useTranslate('chat'); + + const showTokenEmptyError = useCallback(() => { + messageApi.error(t('tokenError')); + }, [messageApi, t]); + return { showTokenEmptyError, contextHolder }; +}; + +const getUrlWithToken = (token: string) => { + const { protocol, host } = window.location; + return `${protocol}//${host}/chat/share?shared_id=${token}`; +}; + +const useFetchTokenListBeforeOtherStep = (dialogId: string) => { + const { showTokenEmptyError, contextHolder } = useShowTokenEmptyError(); + + const listToken = useListToken(); + const tokenList = useSelectTokenList(); + + const token = + Array.isArray(tokenList) && tokenList.length > 0 ? tokenList[0].token : ''; + + const handleOperate = useCallback(async () => { + const data = await listToken(dialogId); + const list = data.data; + if (data.retcode === 0 && Array.isArray(list) && list.length > 0) { + return list[0]?.token; + } else { + showTokenEmptyError(); + return false; + } + }, [dialogId, listToken, showTokenEmptyError]); + + return { + token, + contextHolder, + handleOperate, + }; +}; + +export const useShowEmbedModal = (dialogId: string) => { + const { + visible: embedVisible, + hideModal: hideEmbedModal, + showModal: showEmbedModal, + } = useSetModalState(); + + const { handleOperate, token, contextHolder } = + useFetchTokenListBeforeOtherStep(dialogId); + + const handleShowEmbedModal = useCallback(async () => { + const succeed = await handleOperate(); + if (succeed) { + showEmbedModal(); + } + }, [handleOperate, showEmbedModal]); + + return { + showEmbedModal: handleShowEmbedModal, + hideEmbedModal, + embedVisible, + embedToken: token, + errorContextHolder: contextHolder, + }; +}; + +export const usePreviewChat = (dialogId: string) => { + const { handleOperate, contextHolder } = + useFetchTokenListBeforeOtherStep(dialogId); + + const open = useCallback((t: string) => { + window.open(getUrlWithToken(t), '_blank'); + }, []); + + const handlePreview = useCallback(async () => { + const token = await handleOperate(); + if (token) { + open(token); + } + }, [handleOperate, open]); + + return { + handlePreview, + contextHolder, + }; +}; + //#endregion diff --git a/web/src/pages/chat/index.tsx b/web/src/pages/chat/index.tsx index b07897bc18eed52f7a7468ab9282542d6633d377..a6499d53df6df57c381b63c2262ac31ae7d175ee 100644 --- a/web/src/pages/chat/index.tsx +++ b/web/src/pages/chat/index.tsx @@ -1,6 +1,11 @@ import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg'; import RenameModal from '@/components/rename-modal'; -import { DeleteOutlined, EditOutlined, FormOutlined } from '@ant-design/icons'; +import { + CloudOutlined, + DeleteOutlined, + EditOutlined, + FormOutlined, +} from '@ant-design/icons'; import { Avatar, Button, @@ -185,16 +190,16 @@ const Chat = () => { ), }, { type: 'divider' }, - // { - // key: '3', - // onClick: handleShowOverviewModal(dialog), - // label: ( - // <Space> - // <ProfileOutlined /> - // {t('overview')} - // </Space> - // ), - // }, + { + key: '3', + onClick: handleShowOverviewModal(dialog), + label: ( + <Space> + <CloudOutlined /> + {t('overview')} + </Space> + ), + }, ]; return appItems; diff --git a/web/src/pages/chat/model.ts b/web/src/pages/chat/model.ts index 5c302a272e2e938644126f18fd14dfc29a78c50e..e1a6122f4a55d2446da005a34a61186453d6efb3 100644 --- a/web/src/pages/chat/model.ts +++ b/web/src/pages/chat/model.ts @@ -202,7 +202,7 @@ const model: DvaModel<ChatModelState> = { payload: data.data, }); } - return data.retcode; + return data; }, *removeToken({ payload }, { call, put }) { const { data } = yield call( diff --git a/web/src/pages/chat/share/large.tsx b/web/src/pages/chat/share/large.tsx index 1e510af66086d089090ac67b42f12ba7d9154da9..1a5ba45255d4eb6b974b4bdf95fa5eef108960a1 100644 --- a/web/src/pages/chat/share/large.tsx +++ b/web/src/pages/chat/share/large.tsx @@ -6,10 +6,10 @@ import { Avatar, Button, Flex, Input, Skeleton, Spin } from 'antd'; import classNames from 'classnames'; import { useSelectConversationLoading } from '../hooks'; +import HightLightMarkdown from '@/components/highlight-markdown'; import React, { ChangeEventHandler, forwardRef } from 'react'; import { IClientConversation } from '../interface'; import styles from './index.less'; -import SharedMarkdown from './shared-markdown'; const MessageItem = ({ item }: { item: Message }) => { const isAssistant = item.role === MessageType.Assistant; @@ -46,7 +46,7 @@ const MessageItem = ({ item }: { item: Message }) => { <b>{isAssistant ? '' : 'You'}</b> <div className={styles.messageText}> {item.content !== '' ? ( - <SharedMarkdown content={item.content}></SharedMarkdown> + <HightLightMarkdown>{item.content}</HightLightMarkdown> ) : ( <Skeleton active className={styles.messageEmpty} /> )} diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index 39d30da4212d606a5e73d312219aa837c05a6612..267831db6b9c94570cb53e671ec666f27b4ae199 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -98,8 +98,8 @@ request.interceptors.request.use((url: string, options: any) => { url, options: { ...options, - // data, - // params, + data, + params, headers: { ...(options.skipToken ? undefined : { [Authorization]: authorization }), ...options.headers,