From 1dada69daadf7cabd6d9cfebb08b269a379630db Mon Sep 17 00:00:00 2001 From: balibabu Date: Thu, 18 Apr 2024 19:27:53 +0800 Subject: [PATCH] fix: replace some pictures of chunk method #437 (#438) ### What problem does this PR solve? some chunk method pictures are not in English #437 feat: set the height of both html and body to 100% feat: add SharedChat feat: add shared hooks ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --- web/src/assets/svg/chunk-method/law-02.svg | 116 ++++++----- web/src/assets/svg/chunk-method/manual-02.svg | 131 ++++++------ web/src/assets/svg/chunk-method/manual-04.svg | 115 ++++++----- web/src/assets/svg/chunk-method/qa-01.svg | 173 +++++++++------- web/src/assets/svg/chunk-method/qa-02.svg | 118 +++++------ web/src/assets/svg/chunk-method/resume-02.svg | 117 ++++++----- web/src/assets/svg/chunk-method/table-01.svg | 108 +++++----- web/src/assets/svg/chunk-method/table-02.svg | 172 ++++++---------- web/src/global.less | 13 ++ web/src/hooks/chatHooks.ts | 77 ++++++- web/src/pages/chat/chat-container/index.less | 44 ++-- web/src/pages/chat/chat-container/index.tsx | 164 +-------------- .../pages/chat/chat-overview-modal/index.tsx | 28 ++- web/src/pages/chat/hooks.ts | 7 +- .../pages/chat/markdown-content/index.less | 25 +++ web/src/pages/chat/markdown-content/index.tsx | 173 ++++++++++++++++ web/src/pages/chat/model.ts | 20 +- web/src/pages/chat/share/index.less | 50 +++++ web/src/pages/chat/share/index.tsx | 53 +++++ web/src/pages/chat/share/large.tsx | 122 +++++++++++ web/src/pages/chat/share/shared-markdown.tsx | 32 +++ web/src/pages/chat/shared-hooks.ts | 192 ++++++++++++++++++ web/src/routes.ts | 5 + web/src/services/chatService.ts | 2 +- web/src/utils/commonUtil.ts | 5 + web/src/utils/request.ts | 7 +- 26 files changed, 1336 insertions(+), 733 deletions(-) create mode 100644 web/src/pages/chat/markdown-content/index.less create mode 100644 web/src/pages/chat/markdown-content/index.tsx create mode 100644 web/src/pages/chat/share/index.less create mode 100644 web/src/pages/chat/share/index.tsx create mode 100644 web/src/pages/chat/share/large.tsx create mode 100644 web/src/pages/chat/share/shared-markdown.tsx create mode 100644 web/src/pages/chat/shared-hooks.ts diff --git a/web/src/assets/svg/chunk-method/law-02.svg b/web/src/assets/svg/chunk-method/law-02.svg index 5aedb4569..5ddb6910e 100644 --- a/web/src/assets/svg/chunk-method/law-02.svg +++ b/web/src/assets/svg/chunk-method/law-02.svg @@ -1,80 +1,78 @@ - - - + + - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + - - + + - - - + + - - + + - - + + - - + + - + \ No newline at end of file diff --git a/web/src/assets/svg/chunk-method/manual-02.svg b/web/src/assets/svg/chunk-method/manual-02.svg index 632ca8768..72c319dcb 100644 --- a/web/src/assets/svg/chunk-method/manual-02.svg +++ b/web/src/assets/svg/chunk-method/manual-02.svg @@ -1,79 +1,94 @@ - - - + + - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + - - + + - - - + + - - + + - - + + - - + + - + \ No newline at end of file diff --git a/web/src/assets/svg/chunk-method/manual-04.svg b/web/src/assets/svg/chunk-method/manual-04.svg index 0dc410e10..a7641ea82 100644 --- a/web/src/assets/svg/chunk-method/manual-04.svg +++ b/web/src/assets/svg/chunk-method/manual-04.svg @@ -1,79 +1,78 @@ - - - + + - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + - - + + - - - + + - - + + - - + + - - + + - + \ No newline at end of file diff --git a/web/src/assets/svg/chunk-method/qa-01.svg b/web/src/assets/svg/chunk-method/qa-01.svg index b61810c17..350fea45f 100644 --- a/web/src/assets/svg/chunk-method/qa-01.svg +++ b/web/src/assets/svg/chunk-method/qa-01.svg @@ -1,76 +1,97 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/svg/chunk-method/qa-02.svg b/web/src/assets/svg/chunk-method/qa-02.svg index bc0b9fb74..05510c33a 100644 --- a/web/src/assets/svg/chunk-method/qa-02.svg +++ b/web/src/assets/svg/chunk-method/qa-02.svg @@ -1,97 +1,97 @@ - - - + + - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - + - - + - - - - - - - - - - - - - + - - + - - + + - - - + + - - + + - - + + - - + + - + \ No newline at end of file diff --git a/web/src/assets/svg/chunk-method/resume-02.svg b/web/src/assets/svg/chunk-method/resume-02.svg index a44d851c8..db8c80a32 100644 --- a/web/src/assets/svg/chunk-method/resume-02.svg +++ b/web/src/assets/svg/chunk-method/resume-02.svg @@ -1,95 +1,94 @@ - - - + + - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - + - - + - - - - - - - - - - - - - + - - + - - + + - - - + + - - + + - - + + - - + + - + \ No newline at end of file diff --git a/web/src/assets/svg/chunk-method/table-01.svg b/web/src/assets/svg/chunk-method/table-01.svg index 2dd6baa1f..8b741aca7 100644 --- a/web/src/assets/svg/chunk-method/table-01.svg +++ b/web/src/assets/svg/chunk-method/table-01.svg @@ -1,22 +1,21 @@ - - - + + - + - - - - - - - - + + + + + + + + - + @@ -25,57 +24,70 @@ - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - + - - + + - - + - - + + - - + + - - + + - + \ No newline at end of file diff --git a/web/src/assets/svg/chunk-method/table-02.svg b/web/src/assets/svg/chunk-method/table-02.svg index b3fe562eb..b288ed4ef 100644 --- a/web/src/assets/svg/chunk-method/table-02.svg +++ b/web/src/assets/svg/chunk-method/table-02.svg @@ -1,151 +1,93 @@ - - - + + - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - + - - + - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + - - - + + - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/web/src/global.less b/web/src/global.less index 5a3fd95f7..d5e17323c 100644 --- a/web/src/global.less +++ b/web/src/global.less @@ -1,6 +1,19 @@ @import url(./inter.less); +html { + height: 100%; +} + body { font-family: Inter; margin: 0; + height: 100%; +} + +#root { + height: 100%; +} + +.ant-app { + height: 100%; } diff --git a/web/src/hooks/chatHooks.ts b/web/src/hooks/chatHooks.ts index 90d419d21..3f33f6b0a 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 } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'umi'; export const useFetchDialogList = () => { @@ -248,3 +248,78 @@ export const useSelectStats = () => { }; //#endregion + +//#region shared chat + +export const useCreateSharedConversation = () => { + const dispatch = useDispatch(); + + const createSharedConversation = useCallback( + (userId?: string) => { + return dispatch({ + type: 'chatModel/createExternalConversation', + payload: { userId }, + }); + }, + [dispatch], + ); + + return createSharedConversation; +}; + +export const useFetchSharedConversation = () => { + const dispatch = useDispatch(); + + const fetchSharedConversation = useCallback( + (conversationId: string) => { + return dispatch({ + type: 'chatModel/getExternalConversation', + payload: conversationId, + }); + }, + [dispatch], + ); + + return fetchSharedConversation; +}; + +export const useCompleteSharedConversation = () => { + const dispatch = useDispatch(); + + const completeSharedConversation = useCallback( + (payload: any) => { + return dispatch({ + type: 'chatModel/completeExternalConversation', + payload: payload, + }); + }, + [dispatch], + ); + + 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/pages/chat/chat-container/index.less b/web/src/pages/chat/chat-container/index.less index b521e4b66..0ade14b05 100644 --- a/web/src/pages/chat/chat-container/index.less +++ b/web/src/pages/chat/chat-container/index.less @@ -33,9 +33,9 @@ .messageEmpty { width: 300px; } - .referenceIcon { - padding: 0 6px; - } + // .referenceIcon { + // padding: 0 6px; + // } } .messageItemLeft { @@ -46,24 +46,24 @@ text-align: right; } -.referencePopoverWrapper { - max-width: 50vw; -} +// .referencePopoverWrapper { +// max-width: 50vw; +// } -.referenceChunkImage { - width: 10vw; - object-fit: contain; -} +// .referenceChunkImage { +// width: 10vw; +// object-fit: contain; +// } -.referenceImagePreview { - max-width: 45vw; - max-height: 45vh; -} -.chunkContentText { - .chunkText; - max-height: 45vh; - overflow-y: auto; -} -.documentLink { - padding: 0; -} +// .referenceImagePreview { +// max-width: 45vw; +// max-height: 45vh; +// } +// .chunkContentText { +// .chunkText; +// max-height: 45vh; +// overflow-y: auto; +// } +// .documentLink { +// padding: 0; +// } diff --git a/web/src/pages/chat/chat-container/index.tsx b/web/src/pages/chat/chat-container/index.tsx index 128bbe69a..e57599a65 100644 --- a/web/src/pages/chat/chat-container/index.tsx +++ b/web/src/pages/chat/chat-container/index.tsx @@ -1,5 +1,4 @@ import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg'; -import Image from '@/components/image'; import NewDocumentLink from '@/components/new-document-link'; import DocumentPreviewer from '@/components/pdf-previewer'; import { MessageType } from '@/constants/chat'; @@ -7,7 +6,6 @@ import { useSelectFileThumbnails } from '@/hooks/knowledgeHook'; import { useSelectUserInfo } from '@/hooks/userSettingHook'; import { IReference, Message } from '@/interfaces/database/chat'; import { IChunk } from '@/interfaces/database/knowledge'; -import { InfoCircleOutlined } from '@ant-design/icons'; import { Avatar, Button, @@ -15,18 +13,11 @@ import { Flex, Input, List, - Popover, Skeleton, - Space, Spin, } from 'antd'; import classNames from 'classnames'; -import { useCallback, useMemo } from 'react'; -import Markdown from 'react-markdown'; -import reactStringReplace from 'react-string-replace'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import remarkGfm from 'remark-gfm'; -import { visitParents } from 'unist-util-visit-parents'; +import { useMemo } from 'react'; import { useClickDrawer, useFetchConversationOnMount, @@ -35,33 +26,13 @@ import { useSelectConversationLoading, useSendMessage, } from '../hooks'; +import MarkdownContent from '../markdown-content'; import SvgIcon from '@/components/svg-icon'; import { useTranslate } from '@/hooks/commonHooks'; import { getExtension, isPdf } from '@/utils/documentUtils'; import styles from './index.less'; -const reg = /(#{2}\d+\${2})/g; - -const getChunkIndex = (match: string) => Number(match.slice(2, -2)); - -const rehypeWrapReference = () => { - return function wrapTextTransform(tree: any) { - visitParents(tree, 'text', (node, ancestors) => { - const latestAncestor = ancestors.at(-1); - if ( - latestAncestor.tagName !== 'custom-typography' && - latestAncestor.tagName !== 'code' - ) { - node.type = 'element'; - node.tagName = 'custom-typography'; - node.properties = {}; - node.children = [{ type: 'text', value: node.value }]; - } - }); - }; -}; - const MessageItem = ({ item, reference, @@ -76,100 +47,6 @@ const MessageItem = ({ const isAssistant = item.role === MessageType.Assistant; - const handleDocumentButtonClick = useCallback( - (documentId: string, chunk: IChunk, isPdf: boolean) => () => { - if (!isPdf) { - return; - } - clickDocumentButton(documentId, chunk); - }, - [clickDocumentButton], - ); - - const getPopoverContent = useCallback( - (chunkIndex: number) => { - const chunks = reference?.chunks ?? []; - const chunkItem = chunks[chunkIndex]; - const document = reference?.doc_aggs.find( - (x) => x?.doc_id === chunkItem?.doc_id, - ); - const documentId = document?.doc_id; - const fileThumbnail = documentId ? fileThumbnails[documentId] : ''; - const fileExtension = documentId ? getExtension(document?.doc_name) : ''; - const imageId = chunkItem?.img_id; - return ( - - {imageId && ( - - } - > - - - )} - -
- {documentId && ( - - {fileThumbnail ? ( - - ) : ( - - )} - - - )} -
-
- ); - }, - [reference, fileThumbnails, handleDocumentButtonClick], - ); - - const renderReference = useCallback( - (text: string) => { - return reactStringReplace(text, reg, (match, i) => { - const chunkIndex = getChunkIndex(match); - return ( - - - - ); - }); - }, - [getPopoverContent], - ); - const referenceDocumentList = useMemo(() => { return reference?.doc_aggs ?? []; }, [reference?.doc_aggs]); @@ -207,38 +84,11 @@ const MessageItem = ({ {isAssistant ? '' : userInfo.nickname}
{item.content !== '' ? ( - renderReference(children), - code(props: any) { - const { children, className, node, ...rest } = props; - const match = /language-(\w+)/.exec(className || ''); - return match ? ( - - {String(children).replace(/\n$/, '')} - - ) : ( - - {children} - - ); - }, - } as any - } - > - {item.content} - + ) : ( )} diff --git a/web/src/pages/chat/chat-overview-modal/index.tsx b/web/src/pages/chat/chat-overview-modal/index.tsx index 9d3336dc5..c8a1c4126 100644 --- a/web/src/pages/chat/chat-overview-modal/index.tsx +++ b/web/src/pages/chat/chat-overview-modal/index.tsx @@ -1,11 +1,15 @@ +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 styles from './index.less'; @@ -20,6 +24,10 @@ const ChatOverviewModal = ({ }: IModalProps & { dialog: IDialog }) => { const { t } = useTranslate('chat'); const chartList = useSelectChartStatsList(); + const { urlWithToken, createUrlToken, token } = useCreatePublicUrlToken( + dialog.id, + visible, + ); const { visible: apiKeyVisible, @@ -45,14 +53,20 @@ const ChatOverviewModal = ({ {t('publicUrl')} - - This is a copyable text. - + + {urlWithToken} + + + + + + + - - - - diff --git a/web/src/pages/chat/hooks.ts b/web/src/pages/chat/hooks.ts index 10100b48c..cf5c85831 100644 --- a/web/src/pages/chat/hooks.ts +++ b/web/src/pages/chat/hooks.ts @@ -715,6 +715,8 @@ export const useGetSendButtonDisabled = () => { type RangeValue = [Dayjs | null, Dayjs | null] | null; +const getDay = (date: Dayjs) => date.format('YYYY-MM-DD'); + export const useFetchStatsOnMount = (visible: boolean) => { const fetchStats = useFetchStats(); const [pickerValue, setPickerValue] = useState([ @@ -724,7 +726,10 @@ export const useFetchStatsOnMount = (visible: boolean) => { useEffect(() => { if (visible && Array.isArray(pickerValue) && pickerValue[0]) { - fetchStats({ fromDate: pickerValue[0], toDate: pickerValue[1] }); + fetchStats({ + fromDate: getDay(pickerValue[0]), + toDate: getDay(pickerValue[1] ?? dayjs()), + }); } }, [fetchStats, pickerValue, visible]); diff --git a/web/src/pages/chat/markdown-content/index.less b/web/src/pages/chat/markdown-content/index.less new file mode 100644 index 000000000..ee1301bff --- /dev/null +++ b/web/src/pages/chat/markdown-content/index.less @@ -0,0 +1,25 @@ +.referencePopoverWrapper { + max-width: 50vw; +} + +.referenceChunkImage { + width: 10vw; + object-fit: contain; +} + +.referenceImagePreview { + max-width: 45vw; + max-height: 45vh; +} +.chunkContentText { + .chunkText; + max-height: 45vh; + overflow-y: auto; +} +.documentLink { + padding: 0; +} + +.referenceIcon { + padding: 0 6px; +} diff --git a/web/src/pages/chat/markdown-content/index.tsx b/web/src/pages/chat/markdown-content/index.tsx new file mode 100644 index 000000000..c32166f19 --- /dev/null +++ b/web/src/pages/chat/markdown-content/index.tsx @@ -0,0 +1,173 @@ +import Image from '@/components/image'; +import SvgIcon from '@/components/svg-icon'; +import { useSelectFileThumbnails } from '@/hooks/knowledgeHook'; +import { IReference } from '@/interfaces/database/chat'; +import { IChunk } from '@/interfaces/database/knowledge'; +import { getExtension } from '@/utils/documentUtils'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Button, Flex, Popover, Space } from 'antd'; +import { useCallback } from 'react'; +import Markdown from 'react-markdown'; +import reactStringReplace from 'react-string-replace'; +import SyntaxHighlighter from 'react-syntax-highlighter'; +import remarkGfm from 'remark-gfm'; +import { visitParents } from 'unist-util-visit-parents'; + +import styles from './index.less'; + +const reg = /(#{2}\d+\${2})/g; + +const getChunkIndex = (match: string) => Number(match.slice(2, -2)); +// TODO: The display of the table is inconsistent with the display previously placed in the MessageItem. +const MarkdownContent = ({ + reference, + clickDocumentButton, + content, +}: { + content: string; + reference: IReference; + clickDocumentButton: (documentId: string, chunk: IChunk) => void; +}) => { + const fileThumbnails = useSelectFileThumbnails(); + + const handleDocumentButtonClick = useCallback( + (documentId: string, chunk: IChunk, isPdf: boolean) => () => { + if (!isPdf) { + return; + } + clickDocumentButton(documentId, chunk); + }, + [clickDocumentButton], + ); + + const rehypeWrapReference = () => { + return function wrapTextTransform(tree: any) { + visitParents(tree, 'text', (node, ancestors) => { + const latestAncestor = ancestors.at(-1); + if ( + latestAncestor.tagName !== 'custom-typography' && + latestAncestor.tagName !== 'code' + ) { + node.type = 'element'; + node.tagName = 'custom-typography'; + node.properties = {}; + node.children = [{ type: 'text', value: node.value }]; + } + }); + }; + }; + + const getPopoverContent = useCallback( + (chunkIndex: number) => { + const chunks = reference?.chunks ?? []; + const chunkItem = chunks[chunkIndex]; + const document = reference?.doc_aggs.find( + (x) => x?.doc_id === chunkItem?.doc_id, + ); + const documentId = document?.doc_id; + const fileThumbnail = documentId ? fileThumbnails[documentId] : ''; + const fileExtension = documentId ? getExtension(document?.doc_name) : ''; + const imageId = chunkItem?.img_id; + return ( + + {imageId && ( + + } + > + + + )} + +
+ {documentId && ( + + {fileThumbnail ? ( + + ) : ( + + )} + + + )} +
+
+ ); + }, + [reference, fileThumbnails, handleDocumentButtonClick], + ); + + const renderReference = useCallback( + (text: string) => { + return reactStringReplace(text, reg, (match, i) => { + const chunkIndex = getChunkIndex(match); + return ( + + + + ); + }); + }, + [getPopoverContent], + ); + + return ( + + renderReference(children), + code(props: any) { + const { children, className, node, ...rest } = props; + const match = /language-(\w+)/.exec(className || ''); + return match ? ( + + {String(children).replace(/\n$/, '')} + + ) : ( + + {children} + + ); + }, + } as any + } + > + {content} + + ); +}; + +export default MarkdownContent; diff --git a/web/src/pages/chat/model.ts b/web/src/pages/chat/model.ts index c2498a012..5c302a272 100644 --- a/web/src/pages/chat/model.ts +++ b/web/src/pages/chat/model.ts @@ -158,7 +158,7 @@ const model: DvaModel = { } return data; }, - *completeConversation({ payload }, { call, put }) { + *completeConversation({ payload }, { call }) { const { data } = yield call(chatService.completeConversation, payload); // if (data.retcode === 0) { // yield put({ @@ -192,7 +192,7 @@ const model: DvaModel = { }); message.success(i18n.t('message.created')); } - return data.retcode; + return data; }, *listToken({ payload }, { call, put }) { const { data } = yield call(chatService.listToken, payload); @@ -232,13 +232,13 @@ const model: DvaModel = { chatService.createExternalConversation, payload, ); - if (data.retcode === 0) { - yield put({ - type: 'getExternalConversation', - payload: { conversation_id: payload.conversationId }, - }); - } - return data.retcode; + // if (data.retcode === 0) { + // yield put({ + // type: 'getExternalConversation', + // payload: data.data.id, + // }); + // } + return data; }, *getExternalConversation({ payload }, { call }) { const { data } = yield call( @@ -246,7 +246,7 @@ const model: DvaModel = { null, payload, ); - return data.retcode; + return data; }, *completeExternalConversation({ payload }, { call }) { const { data } = yield call( diff --git a/web/src/pages/chat/share/index.less b/web/src/pages/chat/share/index.less new file mode 100644 index 000000000..54e555011 --- /dev/null +++ b/web/src/pages/chat/share/index.less @@ -0,0 +1,50 @@ +.chatWrapper { + height: 100%; +} + +.chatContainer { + padding: 10px; + box-sizing: border-box; + height: 100%; + .messageContainer { + overflow-y: auto; + padding-right: 6px; + } +} + +.messageItem { + padding: 24px 0; + .messageItemSection { + display: inline-block; + } + .messageItemSectionLeft { + width: 70%; + } + .messageItemSectionRight { + width: 40%; + } + .messageItemContent { + display: inline-flex; + gap: 20px; + } + .messageItemContentReverse { + flex-direction: row-reverse; + } + .messageText { + .chunkText(); + padding: 0 14px; + background-color: rgba(249, 250, 251, 1); + word-break: break-all; + } + .messageEmpty { + width: 300px; + } +} + +.messageItemLeft { + text-align: left; +} + +.messageItemRight { + text-align: right; +} diff --git a/web/src/pages/chat/share/index.tsx b/web/src/pages/chat/share/index.tsx new file mode 100644 index 000000000..00d91cdfe --- /dev/null +++ b/web/src/pages/chat/share/index.tsx @@ -0,0 +1,53 @@ +import { useEffect } from 'react'; +import { + useCreateSharedConversationOnMount, + useSelectCurrentSharedConversation, + useSendSharedMessage, +} from '../shared-hooks'; +import ChatContainer from './large'; + +import styles from './index.less'; + +const SharedChat = () => { + const { conversationId } = useCreateSharedConversationOnMount(); + const { + currentConversation, + addNewestConversation, + removeLatestMessage, + ref, + loading, + setCurrentConversation, + } = useSelectCurrentSharedConversation(conversationId); + + const { + handlePressEnter, + handleInputChange, + value, + loading: sendLoading, + } = useSendSharedMessage( + currentConversation, + addNewestConversation, + removeLatestMessage, + setCurrentConversation, + ); + + useEffect(() => { + console.info(location.href); + }, []); + + return ( +
+ +
+ ); +}; + +export default SharedChat; diff --git a/web/src/pages/chat/share/large.tsx b/web/src/pages/chat/share/large.tsx new file mode 100644 index 000000000..1e510af66 --- /dev/null +++ b/web/src/pages/chat/share/large.tsx @@ -0,0 +1,122 @@ +import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg'; +import { MessageType } from '@/constants/chat'; +import { useTranslate } from '@/hooks/commonHooks'; +import { Message } from '@/interfaces/database/chat'; +import { Avatar, Button, Flex, Input, Skeleton, Spin } from 'antd'; +import classNames from 'classnames'; +import { useSelectConversationLoading } from '../hooks'; + +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; + + return ( +
+
+
+ {item.role === MessageType.User ? ( + + ) : ( + + )} + + {isAssistant ? '' : 'You'} +
+ {item.content !== '' ? ( + + ) : ( + + )} +
+
+
+
+
+ ); +}; + +interface IProps { + handlePressEnter(): void; + handleInputChange: ChangeEventHandler; + value: string; + loading: boolean; + sendLoading: boolean; + conversation: IClientConversation; + ref: React.LegacyRef; +} + +const ChatContainer = ( + { + handlePressEnter, + handleInputChange, + value, + loading: sendLoading, + conversation, + }: IProps, + ref: React.LegacyRef, +) => { + const loading = useSelectConversationLoading(); + const { t } = useTranslate('chat'); + + return ( + <> + + +
+ + {conversation?.message?.map((message) => { + return ( + + ); + })} + +
+
+ + + {t('send')} + + } + onPressEnter={handlePressEnter} + onChange={handleInputChange} + /> + + + ); +}; + +export default forwardRef(ChatContainer); diff --git a/web/src/pages/chat/share/shared-markdown.tsx b/web/src/pages/chat/share/shared-markdown.tsx new file mode 100644 index 000000000..2c1a3c040 --- /dev/null +++ b/web/src/pages/chat/share/shared-markdown.tsx @@ -0,0 +1,32 @@ +import Markdown from 'react-markdown'; +import SyntaxHighlighter from 'react-syntax-highlighter'; +import remarkGfm from 'remark-gfm'; + +const SharedMarkdown = ({ content }: { content: string }) => { + return ( + + {String(children).replace(/\n$/, '')} + + ) : ( + + {children} + + ); + }, + } as any + } + > + {content} + + ); +}; + +export default SharedMarkdown; diff --git a/web/src/pages/chat/shared-hooks.ts b/web/src/pages/chat/shared-hooks.ts new file mode 100644 index 000000000..bb511a0ef --- /dev/null +++ b/web/src/pages/chat/shared-hooks.ts @@ -0,0 +1,192 @@ +import { MessageType } from '@/constants/chat'; +import { + useCompleteSharedConversation, + useCreateSharedConversation, + useFetchSharedConversation, +} from '@/hooks/chatHooks'; +import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; +import omit from 'lodash/omit'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useState, +} from 'react'; +import { useSearchParams } from 'umi'; +import { v4 as uuid } from 'uuid'; +import { useHandleMessageInputChange, useScrollToBottom } from './hooks'; +import { IClientConversation, IMessage } from './interface'; + +export const useCreateSharedConversationOnMount = () => { + const [currentQueryParameters] = useSearchParams(); + const [conversationId, setConversationId] = useState(''); + + const createConversation = useCreateSharedConversation(); + const sharedId = currentQueryParameters.get('shared_id'); + const userId = currentQueryParameters.get('user_id'); + + const setConversation = useCallback(async () => { + console.info(sharedId); + if (sharedId) { + const data = await createConversation(userId ?? undefined); + const id = data.data?.id; + if (id) { + setConversationId(id); + } + } + }, [createConversation, sharedId, userId]); + + useEffect(() => { + setConversation(); + }, [setConversation]); + + return { conversationId }; +}; + +export const useSelectCurrentSharedConversation = (conversationId: string) => { + const [currentConversation, setCurrentConversation] = + useState({} as IClientConversation); + const fetchConversation = useFetchSharedConversation(); + const loading = useOneNamespaceEffectsLoading('chatModel', [ + 'getExternalConversation', + ]); + + const ref = useScrollToBottom(currentConversation); + + const addNewestConversation = useCallback((message: string) => { + setCurrentConversation((pre) => { + return { + ...pre, + message: [ + ...(pre.message ?? []), + { + role: MessageType.User, + content: message, + id: uuid(), + } as IMessage, + { + role: MessageType.Assistant, + content: '', + id: uuid(), + reference: [], + } as IMessage, + ], + }; + }); + }, []); + + const removeLatestMessage = useCallback(() => { + setCurrentConversation((pre) => { + const nextMessages = pre.message.slice(0, -2); + return { + ...pre, + message: nextMessages, + }; + }); + }, []); + + const fetchConversationOnMount = useCallback(async () => { + if (conversationId) { + const data = await fetchConversation(conversationId); + if (data.retcode === 0) { + setCurrentConversation(data.data); + } + } + }, [conversationId, fetchConversation]); + + useEffect(() => { + fetchConversationOnMount(); + }, [fetchConversationOnMount]); + + return { + currentConversation, + addNewestConversation, + removeLatestMessage, + loading, + ref, + setCurrentConversation, + }; +}; + +export const useSendSharedMessage = ( + conversation: IClientConversation, + addNewestConversation: (message: string) => void, + removeLatestMessage: () => void, + setCurrentConversation: Dispatch>, +) => { + const conversationId = conversation.id; + const loading = useOneNamespaceEffectsLoading('chatModel', [ + 'completeExternalConversation', + ]); + const setConversation = useCreateSharedConversation(); + const { handleInputChange, value, setValue } = useHandleMessageInputChange(); + + const fetchConversation = useFetchSharedConversation(); + const completeConversation = useCompleteSharedConversation(); + + const sendMessage = useCallback( + async (message: string, id?: string) => { + const retcode = await completeConversation({ + conversation_id: id ?? conversationId, + messages: [ + ...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')), + { + role: MessageType.User, + content: message, + }, + ], + }); + + if (retcode === 0) { + const data = await fetchConversation(conversationId); + if (data.retcode === 0) { + setCurrentConversation(data.data); + } + } else { + // cancel loading + setValue(message); + removeLatestMessage(); + } + }, + [ + conversationId, + conversation?.message, + fetchConversation, + removeLatestMessage, + setValue, + completeConversation, + setCurrentConversation, + ], + ); + + const handleSendMessage = useCallback( + async (message: string) => { + if (conversationId !== '') { + sendMessage(message); + } else { + const data = await setConversation('user id'); + if (data.retcode === 0) { + const id = data.data.id; + sendMessage(message, id); + } + } + }, + [conversationId, setConversation, sendMessage], + ); + + const handlePressEnter = () => { + if (!loading) { + setValue(''); + addNewestConversation(value); + handleSendMessage(value.trim()); + } + }; + + return { + handlePressEnter, + handleInputChange, + value, + loading, + }; +}; diff --git a/web/src/routes.ts b/web/src/routes.ts index b6d9e9bcc..ff6187052 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -4,6 +4,11 @@ const routes = [ component: '@/pages/login', layout: false, }, + { + path: '/chat/share', + component: '@/pages/chat/share', + layout: false, + }, { path: '/', component: '@/layouts', diff --git a/web/src/services/chatService.ts b/web/src/services/chatService.ts index 0b4567560..496ed6754 100644 --- a/web/src/services/chatService.ts +++ b/web/src/services/chatService.ts @@ -76,7 +76,7 @@ const methods = { }, createExternalConversation: { url: createExternalConversation, - method: 'post', + method: 'get', }, getExternalConversation: { url: getExternalConversation, diff --git a/web/src/utils/commonUtil.ts b/web/src/utils/commonUtil.ts index 9263e99d0..9cda1a995 100644 --- a/web/src/utils/commonUtil.ts +++ b/web/src/utils/commonUtil.ts @@ -15,3 +15,8 @@ export const convertTheKeysOfTheObjectToSnake = (data: unknown) => { } return data; }; + +export const getSearchValue = (key: string) => { + const params = new URL(document.location as any).searchParams; + return params.get(key); +}; diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index 91e7d4b38..39d30da42 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -4,7 +4,7 @@ import authorizationUtil from '@/utils/authorizationUtil'; import { message, notification } from 'antd'; import { history } from 'umi'; import { RequestMethod, extend } from 'umi-request'; -import { convertTheKeysOfTheObjectToSnake } from './commonUtil'; +import { convertTheKeysOfTheObjectToSnake, getSearchValue } from './commonUtil'; const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.'; // 手动中断请求。errorHandler 抛出的error message @@ -87,7 +87,10 @@ const request: RequestMethod = extend({ }); request.interceptors.request.use((url: string, options: any) => { - const authorization = authorizationUtil.getAuthorization(); + const sharedId = getSearchValue('shared_id'); + const authorization = sharedId + ? 'Bearer ' + sharedId + : authorizationUtil.getAuthorization(); const data = convertTheKeysOfTheObjectToSnake(options.data); const params = convertTheKeysOfTheObjectToSnake(options.params);